mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-10-23 11:27:39 +00:00
Merged 'dev' branch
This commit is contained in:
@@ -39,6 +39,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
@@ -50,6 +51,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
@@ -159,27 +161,36 @@ public class RouterActivity extends AppCompatActivity {
|
||||
if (result) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onError();
|
||||
showUnsupportedUrlDialog(url);
|
||||
}
|
||||
}, this::handleError));
|
||||
}, throwable -> handleError(throwable, url)));
|
||||
}
|
||||
|
||||
private void handleError(final Throwable error) {
|
||||
error.printStackTrace();
|
||||
private void handleError(final Throwable throwable, final String url) {
|
||||
throwable.printStackTrace();
|
||||
|
||||
if (error instanceof ExtractionException) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
if (throwable instanceof ExtractionException) {
|
||||
showUnsupportedUrlDialog(url);
|
||||
} else {
|
||||
ExtractorHelper.handleGeneralException(this, -1, null, error,
|
||||
ExtractorHelper.handleGeneralException(this, -1, url, throwable,
|
||||
UserAction.SOMETHING_ELSE, null);
|
||||
finish();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onError() {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
private void showUnsupportedUrlDialog(final String url) {
|
||||
final Context context = getThemeWrapperContext();
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.unsupported_url)
|
||||
.setMessage(R.string.unsupported_url_dialog_message)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_share))
|
||||
.setPositiveButton(R.string.open_in_browser,
|
||||
(dialog, which) -> ShareUtils.openUrlInBrowser(this, url))
|
||||
.setNegativeButton(R.string.share,
|
||||
(dialog, which) -> ShareUtils.shareUrl(this, "", url)) // no subject
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.setOnDismissListener(dialog -> finish())
|
||||
.show();
|
||||
}
|
||||
|
||||
protected void onSuccess() {
|
||||
@@ -258,7 +269,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
|
||||
R.layout.preferred_player_dialog_view, null, false);
|
||||
R.layout.single_choice_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
@@ -268,6 +279,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
handleChoice(choice.key);
|
||||
|
||||
// open future streams always like this one, because "always" button was used by user
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
preferences.edit()
|
||||
.putString(getString(R.string.preferred_open_action_key), choice.key)
|
||||
@@ -367,23 +379,50 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key),
|
||||
getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_info_outline)));
|
||||
final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow));
|
||||
final AdapterChoiceItem showInfo = new AdapterChoiceItem(
|
||||
getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_info_outline));
|
||||
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_popup));
|
||||
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_headset));
|
||||
|
||||
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key),
|
||||
getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow)));
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key),
|
||||
getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_popup)));
|
||||
}
|
||||
if (linkType == LinkType.STREAM) {
|
||||
if (isExtVideoEnabled) {
|
||||
// show both "show info" and "video player", they are two different activities
|
||||
returnList.add(showInfo);
|
||||
returnList.add(videoPlayer);
|
||||
} else if (capabilities.contains(VIDEO)
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(context)) {
|
||||
// show only "video player" since the details activity will be opened and the video
|
||||
// will be autoplayed there and "show info" would do the exact same thing
|
||||
returnList.add(videoPlayer);
|
||||
} else {
|
||||
// show only "show info" if video player is not applicable or autoplay is disabled
|
||||
returnList.add(showInfo);
|
||||
}
|
||||
|
||||
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key),
|
||||
getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_headset)));
|
||||
if (capabilities.contains(VIDEO)) {
|
||||
returnList.add(popupPlayer);
|
||||
}
|
||||
if (capabilities.contains(AUDIO)) {
|
||||
returnList.add(backgroundPlayer);
|
||||
}
|
||||
|
||||
} else {
|
||||
returnList.add(showInfo);
|
||||
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
|
||||
returnList.add(videoPlayer);
|
||||
returnList.add(popupPlayer);
|
||||
}
|
||||
if (capabilities.contains(AUDIO) && !isExtAudioEnabled) {
|
||||
returnList.add(backgroundPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
|
||||
@@ -459,7 +498,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
startActivity(intent);
|
||||
|
||||
finish();
|
||||
}, this::handleError)
|
||||
}, throwable -> handleError(throwable, currentUrl))
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -493,7 +532,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
fm.executePendingTransactions();
|
||||
downloadDialog.getDialog().setOnDismissListener(dialog -> finish());
|
||||
}, (@NonNull Throwable throwable) -> onError());
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
showUnsupportedUrlDialog(currentUrl);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -122,6 +122,7 @@ import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
@@ -327,7 +328,7 @@ public class VideoDetailFragment
|
||||
settingsContentObserver = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(final boolean selfChange) {
|
||||
if (activity != null && !PlayerHelper.globalScreenOrientationLocked(activity)) {
|
||||
if (activity != null && !globalScreenOrientationLocked(activity)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
@@ -853,6 +854,9 @@ public class VideoDetailFragment
|
||||
return;
|
||||
}
|
||||
setInitialData(sid, videoUrl, title, queue);
|
||||
if (player != null) {
|
||||
player.disablePreloadingOfCurrentTrack();
|
||||
}
|
||||
startLoading(false, true);
|
||||
}
|
||||
|
||||
@@ -1143,7 +1147,7 @@ public class VideoDetailFragment
|
||||
}
|
||||
|
||||
private boolean isExternalPlayerEnabled() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(getContext())
|
||||
return PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
}
|
||||
|
||||
@@ -1154,23 +1158,7 @@ public class VideoDetailFragment
|
||||
&& !isExternalPlayerEnabled()
|
||||
&& (player == null || player.videoPlayerSelected())
|
||||
&& bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
|
||||
&& isAutoplayAllowedByUser();
|
||||
}
|
||||
|
||||
private boolean isAutoplayAllowedByUser() {
|
||||
if (activity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (PlayerHelper.getAutoplayType(activity)) {
|
||||
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER:
|
||||
return false;
|
||||
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI:
|
||||
return !ListHelper.isMeteredNetwork(activity);
|
||||
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
|
||||
}
|
||||
|
||||
private void addVideoPlayerView() {
|
||||
@@ -1759,9 +1747,6 @@ public class VideoDetailFragment
|
||||
setOverlayPlayPauseImage();
|
||||
|
||||
switch (state) {
|
||||
case BasePlayer.STATE_COMPLETED:
|
||||
restoreDefaultOrientation();
|
||||
break;
|
||||
case BasePlayer.STATE_PLAYING:
|
||||
if (positionView.getAlpha() != 1.0f
|
||||
&& player.getPlayQueue() != null
|
||||
@@ -1865,7 +1850,13 @@ public class VideoDetailFragment
|
||||
}
|
||||
scrollToTop();
|
||||
|
||||
addVideoPlayerView();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addVideoPlayerView();
|
||||
} else {
|
||||
// KitKat needs a delay before addVideoPlayerView call or it reports wrong height in
|
||||
// activity.getWindow().getDecorView().getHeight()
|
||||
new Handler().post(this::addVideoPlayerView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1873,13 +1864,15 @@ public class VideoDetailFragment
|
||||
// In tablet user experience will be better if screen will not be rotated
|
||||
// from landscape to portrait every time.
|
||||
// Just turn on fullscreen mode in landscape orientation
|
||||
if (isLandscape() && DeviceUtils.isTablet(activity)) {
|
||||
// or portrait & unlocked global orientation
|
||||
if (DeviceUtils.isTablet(activity)
|
||||
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
|
||||
player.toggleFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
final int newOrientation = isLandscape()
|
||||
? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||
|
||||
activity.setRequestedOrientation(newOrientation);
|
||||
@@ -2022,9 +2015,8 @@ public class VideoDetailFragment
|
||||
}
|
||||
|
||||
player.checkLandscape();
|
||||
final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(activity);
|
||||
// Let's give a user time to look at video information page if video is not playing
|
||||
if (orientationLocked && !player.isPlaying()) {
|
||||
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
|
||||
player.onPlay();
|
||||
}
|
||||
}
|
||||
|
@@ -52,6 +52,7 @@ import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
@@ -78,7 +79,7 @@ import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovement
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
|
||||
public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage<?>>
|
||||
implements BackPressable {
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
@@ -133,7 +134,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
private Map<Integer, String> menuItemToFilterName;
|
||||
private StreamingService service;
|
||||
private Page nextPage;
|
||||
private String contentCountry;
|
||||
private boolean isSuggestionsEnabled = true;
|
||||
|
||||
private Disposable searchDisposable;
|
||||
@@ -154,6 +154,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
private TextView correctSuggestion;
|
||||
|
||||
private View suggestionsPanel;
|
||||
private boolean suggestionsPanelVisible = false;
|
||||
private RecyclerView suggestionsRecyclerView;
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -204,8 +205,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
= PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
isSuggestionsEnabled = preferences
|
||||
.getBoolean(getString(R.string.show_search_suggestions_key), true);
|
||||
contentCountry = preferences.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_localization_key));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -233,9 +232,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (suggestionDisposable != null) {
|
||||
suggestionDisposable.dispose();
|
||||
}
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
|
||||
@@ -249,8 +246,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportError(getActivity(), e, getActivity().getClass(),
|
||||
getActivity().findViewById(android.R.id.content),
|
||||
ErrorActivity.reportError(getActivity(), e, requireActivity().getClass(),
|
||||
requireActivity().findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"",
|
||||
"", R.string.general_error));
|
||||
@@ -303,26 +300,20 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (suggestionDisposable != null) {
|
||||
suggestionDisposable.dispose();
|
||||
}
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK
|
||||
&& !TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else {
|
||||
Log.e(TAG, "ReCaptcha failed");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK
|
||||
&& !TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else {
|
||||
Log.e(TAG, "ReCaptcha failed");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +331,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
@Override
|
||||
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
|
||||
@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
return getSuggestionMovementFlags(recyclerView, viewHolder);
|
||||
return getSuggestionMovementFlags(viewHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -352,7 +343,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, final int i) {
|
||||
onSuggestionItemSwiped(viewHolder, i);
|
||||
onSuggestionItemSwiped(viewHolder);
|
||||
}
|
||||
}).attachToRecyclerView(suggestionsRecyclerView);
|
||||
|
||||
@@ -627,6 +618,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSuggestionsPanel() called");
|
||||
}
|
||||
suggestionsPanelVisible = true;
|
||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
|
||||
}
|
||||
|
||||
@@ -634,6 +626,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hideSuggestionsPanel() called");
|
||||
}
|
||||
suggestionsPanelVisible = false;
|
||||
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
|
||||
}
|
||||
|
||||
@@ -669,8 +662,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||
if (activity == null || historyRecordManager == null || suggestionPublisher == null
|
||||
|| searchEditText == null || disposables == null) {
|
||||
if (activity == null || historyRecordManager == null || searchEditText == null) {
|
||||
return;
|
||||
}
|
||||
final String query = item.query;
|
||||
@@ -695,7 +687,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (suggestionsPanel.getVisibility() == View.VISIBLE
|
||||
if (suggestionsPanelVisible
|
||||
&& infoListAdapter.getItemsList().size() > 0
|
||||
&& !isLoading.get()) {
|
||||
hideSuggestionsPanel();
|
||||
@@ -742,6 +734,13 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
final Observable<List<SuggestionItem>> network = ExtractorHelper
|
||||
.suggestionsFor(serviceId, query)
|
||||
.onErrorReturn(throwable -> {
|
||||
if (!ExceptionUtils.isNetworkRelated(throwable)) {
|
||||
showSnackBarError(throwable, UserAction.GET_SUGGESTIONS,
|
||||
NewPipe.getNameOfService(serviceId), searchString, 0);
|
||||
}
|
||||
return new ArrayList<>();
|
||||
})
|
||||
.toObservable()
|
||||
.map(strings -> {
|
||||
final List<SuggestionItem> result = new ArrayList<>();
|
||||
@@ -791,28 +790,30 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void search(final String ss, final String[] cf, final String sf) {
|
||||
private void search(final String theSearchString,
|
||||
final String[] theContentFilter,
|
||||
final String theSortFilter) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "search() called with: query = [" + ss + "]");
|
||||
Log.d(TAG, "search() called with: query = [" + theSearchString + "]");
|
||||
}
|
||||
if (ss.isEmpty()) {
|
||||
if (theSearchString.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final StreamingService streamingService = NewPipe.getServiceByUrl(ss);
|
||||
final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString);
|
||||
if (streamingService != null) {
|
||||
showLoading();
|
||||
disposables.add(Observable
|
||||
.fromCallable(() ->
|
||||
NavigationHelper.getIntentByLink(activity, streamingService, ss))
|
||||
.fromCallable(() -> NavigationHelper.getIntentByLink(activity,
|
||||
streamingService, theSearchString))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(intent -> {
|
||||
getFM().popBackStackImmediate();
|
||||
activity.startActivity(intent);
|
||||
}, throwable ->
|
||||
showError(getString(R.string.url_not_supported_toast), false)));
|
||||
showError(getString(R.string.unsupported_url), false)));
|
||||
return;
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
@@ -820,29 +821,27 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
lastSearchedString = this.searchString;
|
||||
this.searchString = ss;
|
||||
this.searchString = theSearchString;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
|
||||
historyRecordManager.onSearched(serviceId, ss)
|
||||
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {
|
||||
},
|
||||
error -> showSnackBarError(error, UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), ss, 0)
|
||||
);
|
||||
suggestionPublisher.onNext(ss);
|
||||
NewPipe.getNameOfService(serviceId), theSearchString, 0)
|
||||
));
|
||||
suggestionPublisher.onNext(theSearchString);
|
||||
startLoading(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startLoading(final boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
disposables.clear();
|
||||
if (searchDisposable != null) {
|
||||
searchDisposable.dispose();
|
||||
}
|
||||
@@ -881,8 +880,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
// TODO: No way to tell if search has more items in the moment
|
||||
return true;
|
||||
return Page.isValid(nextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -895,22 +893,25 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void changeContentFilter(final MenuItem item, final List<String> cf) {
|
||||
this.filterItemCheckedId = item.getItemId();
|
||||
private void changeContentFilter(final MenuItem item, final List<String> theContentFilter) {
|
||||
filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
|
||||
this.contentFilter = new String[]{cf.get(0)};
|
||||
contentFilter = new String[]{theContentFilter.get(0)};
|
||||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, this.contentFilter, sortFilter);
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuery(final int sid, final String ss, final String[] cf, final String sf) {
|
||||
this.serviceId = sid;
|
||||
this.searchString = searchString;
|
||||
this.contentFilter = cf;
|
||||
this.sortFilter = sf;
|
||||
private void setQuery(final int theServiceId,
|
||||
final String theSearchString,
|
||||
final String[] theContentFilter,
|
||||
final String theSortFilter) {
|
||||
serviceId = theServiceId;
|
||||
searchString = theSearchString;
|
||||
contentFilter = theContentFilter;
|
||||
sortFilter = theSortFilter;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -924,7 +925,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
suggestionsRecyclerView.smoothScrollToPosition(0);
|
||||
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
|
||||
|
||||
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||
if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
@@ -1027,7 +1028,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage<?> result) {
|
||||
showListFooter(false);
|
||||
infoListAdapter.addInfoItemList(result.getItems());
|
||||
nextPage = result.getNextPage();
|
||||
@@ -1066,8 +1067,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
// Suggestion item touch helper
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public int getSuggestionMovementFlags(@NonNull final RecyclerView recyclerView,
|
||||
@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
if (position == RecyclerView.NO_POSITION) {
|
||||
return 0;
|
||||
@@ -1078,8 +1078,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
||||
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
|
||||
}
|
||||
|
||||
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
|
||||
final int i) {
|
||||
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
|
||||
final int position = viewHolder.getAdapterPosition();
|
||||
final String query = suggestionListAdapter.getItem(position).query;
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||
|
@@ -180,6 +180,8 @@ public abstract class BasePlayer implements
|
||||
@NonNull
|
||||
protected final HistoryRecordManager recordManager;
|
||||
@NonNull
|
||||
protected final SharedPreferences sharedPreferences;
|
||||
@NonNull
|
||||
protected final CustomTrackSelector trackSelector;
|
||||
@NonNull
|
||||
protected final PlayerDataSource dataSource;
|
||||
@@ -211,6 +213,7 @@ public abstract class BasePlayer implements
|
||||
setupBroadcastReceiver(intentFilter);
|
||||
|
||||
this.recordManager = new HistoryRecordManager(context);
|
||||
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
this.progressUpdateReactor = new SerialDisposable();
|
||||
this.databaseUpdateReactor = new CompositeDisposable();
|
||||
@@ -1246,7 +1249,15 @@ public abstract class BasePlayer implements
|
||||
Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
|
||||
}
|
||||
if (simpleExoPlayer != null) {
|
||||
simpleExoPlayer.seekTo(positionMillis);
|
||||
// prevent invalid positions when fast-forwarding/-rewinding
|
||||
long normalizedPositionMillis = positionMillis;
|
||||
if (normalizedPositionMillis < 0) {
|
||||
normalizedPositionMillis = 0;
|
||||
} else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
|
||||
normalizedPositionMillis = simpleExoPlayer.getDuration();
|
||||
}
|
||||
|
||||
simpleExoPlayer.seekTo(normalizedPositionMillis);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1417,6 +1428,11 @@ public abstract class BasePlayer implements
|
||||
return currentMetadata;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public LoadController getLoadController() {
|
||||
return (LoadController) loadControl;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getVideoUrl() {
|
||||
return currentMetadata == null
|
||||
|
@@ -19,35 +19,18 @@
|
||||
|
||||
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 androidx.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 android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
|
||||
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;
|
||||
@@ -64,7 +47,6 @@ public final class MainPlayer extends Service {
|
||||
|
||||
private VideoPlayerImpl playerImpl;
|
||||
private WindowManager windowManager;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
private final IBinder mBinder = new MainPlayer.LocalBinder();
|
||||
|
||||
@@ -78,30 +60,26 @@ public final class MainPlayer extends Service {
|
||||
// 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";
|
||||
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";
|
||||
static final String ACTION_SHUFFLE
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE";
|
||||
public static final String ACTION_RECREATE_NOTIFICATION
|
||||
= "org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service's LifeCycle
|
||||
@@ -113,9 +91,7 @@ public final class MainPlayer extends Service {
|
||||
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();
|
||||
@@ -143,7 +119,7 @@ public final class MainPlayer extends Service {
|
||||
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
|| intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) {
|
||||
showNotificationAndStartForeground();
|
||||
NotificationUtil.getInstance().createNotificationAndStartForeground(playerImpl, this);
|
||||
}
|
||||
|
||||
playerImpl.handleIntent(intent);
|
||||
@@ -177,7 +153,7 @@ public final class MainPlayer extends Service {
|
||||
// So we should hide the notification at all.
|
||||
// When autoplay enabled such notification flashing is annoying so skip this case
|
||||
if (!autoplayEnabled) {
|
||||
stopForeground(true);
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,11 +208,8 @@ public final class MainPlayer extends Service {
|
||||
playerImpl.removePopupFromView();
|
||||
playerImpl.destroy();
|
||||
}
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
stopForeground(true);
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@@ -275,206 +248,6 @@ public final class MainPlayer extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
|
@@ -0,0 +1,165 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class NotificationConstants {
|
||||
|
||||
private NotificationConstants() { }
|
||||
|
||||
|
||||
public static final int NOTHING = 0;
|
||||
public static final int PREVIOUS = 1;
|
||||
public static final int NEXT = 2;
|
||||
public static final int REWIND = 3;
|
||||
public static final int FORWARD = 4;
|
||||
public static final int SMART_REWIND_PREVIOUS = 5;
|
||||
public static final int SMART_FORWARD_NEXT = 6;
|
||||
public static final int PLAY_PAUSE = 7;
|
||||
public static final int PLAY_PAUSE_BUFFERING = 8;
|
||||
public static final int REPEAT = 9;
|
||||
public static final int SHUFFLE = 10;
|
||||
public static final int CLOSE = 11;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS, SMART_FORWARD_NEXT,
|
||||
PLAY_PAUSE, PLAY_PAUSE_BUFFERING, REPEAT, SHUFFLE, CLOSE})
|
||||
public @interface Action { }
|
||||
|
||||
@DrawableRes
|
||||
public static final int[] ACTION_ICONS = {
|
||||
0,
|
||||
R.drawable.exo_icon_previous,
|
||||
R.drawable.exo_icon_next,
|
||||
R.drawable.exo_icon_rewind,
|
||||
R.drawable.exo_icon_fastforward,
|
||||
R.drawable.exo_icon_previous,
|
||||
R.drawable.exo_icon_next,
|
||||
R.drawable.ic_pause_white_24dp,
|
||||
R.drawable.ic_hourglass_top_white_24dp,
|
||||
R.drawable.exo_icon_repeat_all,
|
||||
R.drawable.exo_icon_shuffle_on,
|
||||
R.drawable.ic_close_white_24dp,
|
||||
};
|
||||
|
||||
|
||||
@Action
|
||||
public static final int[] SLOT_DEFAULTS = {
|
||||
SMART_REWIND_PREVIOUS,
|
||||
PLAY_PAUSE_BUFFERING,
|
||||
SMART_FORWARD_NEXT,
|
||||
REPEAT,
|
||||
CLOSE,
|
||||
};
|
||||
|
||||
@Action
|
||||
public static final int[][] SLOT_ALLOWED_ACTIONS = {
|
||||
new int[] {PREVIOUS, REWIND, SMART_REWIND_PREVIOUS},
|
||||
new int[] {REWIND, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
|
||||
new int[] {NEXT, FORWARD, SMART_FORWARD_NEXT, PLAY_PAUSE, PLAY_PAUSE_BUFFERING},
|
||||
new int[] {NOTHING, PREVIOUS, NEXT, REWIND, FORWARD, SMART_REWIND_PREVIOUS,
|
||||
SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
|
||||
new int[] {NOTHING, NEXT, FORWARD, SMART_FORWARD_NEXT, REPEAT, SHUFFLE, CLOSE},
|
||||
};
|
||||
|
||||
public static final int[] SLOT_PREF_KEYS = {
|
||||
R.string.notification_slot_0_key,
|
||||
R.string.notification_slot_1_key,
|
||||
R.string.notification_slot_2_key,
|
||||
R.string.notification_slot_3_key,
|
||||
R.string.notification_slot_4_key,
|
||||
};
|
||||
|
||||
|
||||
public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2};
|
||||
|
||||
public static final int[] SLOT_COMPACT_PREF_KEYS = {
|
||||
R.string.notification_slot_compact_0_key,
|
||||
R.string.notification_slot_compact_1_key,
|
||||
R.string.notification_slot_compact_2_key,
|
||||
};
|
||||
|
||||
|
||||
public static String getActionName(@NonNull final Context context, @Action final int action) {
|
||||
switch (action) {
|
||||
case PREVIOUS:
|
||||
return context.getString(R.string.exo_controls_previous_description);
|
||||
case NEXT:
|
||||
return context.getString(R.string.exo_controls_next_description);
|
||||
case REWIND:
|
||||
return context.getString(R.string.exo_controls_rewind_description);
|
||||
case FORWARD:
|
||||
return context.getString(R.string.exo_controls_fastforward_description);
|
||||
case SMART_REWIND_PREVIOUS:
|
||||
return Localization.concatenateStrings(
|
||||
context.getString(R.string.exo_controls_rewind_description),
|
||||
context.getString(R.string.exo_controls_previous_description));
|
||||
case SMART_FORWARD_NEXT:
|
||||
return Localization.concatenateStrings(
|
||||
context.getString(R.string.exo_controls_fastforward_description),
|
||||
context.getString(R.string.exo_controls_next_description));
|
||||
case PLAY_PAUSE:
|
||||
return Localization.concatenateStrings(
|
||||
context.getString(R.string.exo_controls_play_description),
|
||||
context.getString(R.string.exo_controls_pause_description));
|
||||
case PLAY_PAUSE_BUFFERING:
|
||||
return Localization.concatenateStrings(
|
||||
context.getString(R.string.exo_controls_play_description),
|
||||
context.getString(R.string.exo_controls_pause_description),
|
||||
context.getString(R.string.notification_action_buffering));
|
||||
case REPEAT:
|
||||
return context.getString(R.string.notification_action_repeat);
|
||||
case SHUFFLE:
|
||||
return context.getString(R.string.notification_action_shuffle);
|
||||
case CLOSE:
|
||||
return context.getString(R.string.close);
|
||||
case NOTHING: default:
|
||||
return context.getString(R.string.notification_action_nothing);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param context the context to use
|
||||
* @param sharedPreferences the shared preferences to query values from
|
||||
* @param slotCount remove indices >= than this value (set to {@code 5} to do nothing, or make
|
||||
* it lower if there are slots with empty actions)
|
||||
* @return a sorted list of the indices of the slots to use as compact slots
|
||||
*/
|
||||
public static List<Integer> getCompactSlotsFromPreferences(
|
||||
@NonNull final Context context,
|
||||
final SharedPreferences sharedPreferences,
|
||||
final int slotCount) {
|
||||
final SortedSet<Integer> compactSlots = new TreeSet<>();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int compactSlot = sharedPreferences.getInt(
|
||||
context.getString(SLOT_COMPACT_PREF_KEYS[i]), Integer.MAX_VALUE);
|
||||
|
||||
if (compactSlot == Integer.MAX_VALUE) {
|
||||
// settings not yet populated, return default values
|
||||
return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS));
|
||||
}
|
||||
|
||||
// a negative value (-1) is set when the user does not want a particular compact slot
|
||||
if (compactSlot >= 0 && compactSlot < slotCount) {
|
||||
compactSlots.add(compactSlot);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(compactSlots);
|
||||
}
|
||||
}
|
@@ -0,0 +1,371 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
||||
|
||||
/**
|
||||
* This is a utility class for player notifications.
|
||||
*
|
||||
* @author cool-student
|
||||
*/
|
||||
public final class NotificationUtil {
|
||||
private static final String TAG = NotificationUtil.class.getSimpleName();
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
private static final int NOTIFICATION_ID = 123789;
|
||||
|
||||
@Nullable private static NotificationUtil instance = null;
|
||||
|
||||
@NotificationConstants.Action
|
||||
private int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
|
||||
|
||||
private NotificationManagerCompat notificationManager;
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
private NotificationUtil() {
|
||||
}
|
||||
|
||||
public static NotificationUtil getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new NotificationUtil();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// NOTIFICATION
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Creates the notification if it does not exist already and recreates it if forceRecreate is
|
||||
* true. Updates the notification with the data in the player.
|
||||
* @param player the player currently open, to take data from
|
||||
* @param forceRecreate whether to force the recreation of the notification even if it already
|
||||
* exists
|
||||
*/
|
||||
synchronized void createNotificationIfNeededAndUpdate(final VideoPlayerImpl player,
|
||||
final boolean forceRecreate) {
|
||||
if (forceRecreate || notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
}
|
||||
updateNotification(player);
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
|
||||
private synchronized NotificationCompat.Builder createNotification(
|
||||
final VideoPlayerImpl player) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "createNotification()");
|
||||
}
|
||||
notificationManager = NotificationManagerCompat.from(player.context);
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(player.context,
|
||||
player.context.getString(R.string.notification_channel_id));
|
||||
|
||||
initializeNotificationSlots(player);
|
||||
|
||||
// count the number of real slots, to make sure compact slots indices are not out of bound
|
||||
int nonNothingSlotCount = 5;
|
||||
if (notificationSlots[3] == NotificationConstants.NOTHING) {
|
||||
--nonNothingSlotCount;
|
||||
}
|
||||
if (notificationSlots[4] == NotificationConstants.NOTHING) {
|
||||
--nonNothingSlotCount;
|
||||
}
|
||||
|
||||
// build the compact slot indices array (need code to convert from Integer... because Java)
|
||||
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
|
||||
player.context, player.sharedPreferences, nonNothingSlotCount);
|
||||
final int[] compactSlots = new int[compactSlotList.size()];
|
||||
for (int i = 0; i < compactSlotList.size(); i++) {
|
||||
compactSlots[i] = compactSlotList.get(i);
|
||||
}
|
||||
|
||||
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(player.mediaSessionManager.getSessionToken())
|
||||
.setShowActionsInCompactView(compactSlots))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setColor(ContextCompat.getColor(player.context, R.color.gray))
|
||||
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||
.setDeleteIntent(PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
|
||||
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the notification builder and the button icons depending on the playback state.
|
||||
* @param player the player currently open, to take data from
|
||||
*/
|
||||
private synchronized void updateNotification(final VideoPlayerImpl player) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateNotification()");
|
||||
}
|
||||
|
||||
// also update content intent, in case the user switched players
|
||||
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.context,
|
||||
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
|
||||
notificationBuilder.setContentTitle(player.getVideoTitle());
|
||||
notificationBuilder.setContentText(player.getUploaderName());
|
||||
notificationBuilder.setTicker(player.getVideoTitle());
|
||||
updateActions(notificationBuilder, player);
|
||||
setLargeIcon(notificationBuilder, player);
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
boolean shouldUpdateBufferingSlot() {
|
||||
if (notificationBuilder.mActions.size() < 3) {
|
||||
// this should never happen, but let's make sure notification actions are populated
|
||||
return true;
|
||||
}
|
||||
|
||||
// only second and third slot could contain PLAY_PAUSE_BUFFERING, update them only if they
|
||||
// are not already in the buffering state (the only one with a null action intent)
|
||||
return (notificationSlots[1] == NotificationConstants.PLAY_PAUSE_BUFFERING
|
||||
&& notificationBuilder.mActions.get(1).actionIntent != null)
|
||||
|| (notificationSlots[2] == NotificationConstants.PLAY_PAUSE_BUFFERING
|
||||
&& notificationBuilder.mActions.get(2).actionIntent != null);
|
||||
}
|
||||
|
||||
|
||||
void createNotificationAndStartForeground(final VideoPlayerImpl player, final Service service) {
|
||||
if (notificationBuilder == null) {
|
||||
notificationBuilder = createNotification(player);
|
||||
}
|
||||
updateNotification(player);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
} else {
|
||||
service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
void cancelNotificationAndStopForeground(final Service service) {
|
||||
service.stopForeground(true);
|
||||
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
notificationManager = null;
|
||||
notificationBuilder = null;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// ACTIONS
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void initializeNotificationSlots(final VideoPlayerImpl player) {
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
notificationSlots[i] = player.sharedPreferences.getInt(
|
||||
player.context.getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||
NotificationConstants.SLOT_DEFAULTS[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void updateActions(final NotificationCompat.Builder builder,
|
||||
final VideoPlayerImpl player) {
|
||||
builder.mActions.clear();
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
addAction(builder, player, notificationSlots[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAction(final NotificationCompat.Builder builder,
|
||||
final VideoPlayerImpl player,
|
||||
@NotificationConstants.Action final int slot) {
|
||||
final NotificationCompat.Action action = getAction(player, slot);
|
||||
if (action != null) {
|
||||
builder.addAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private NotificationCompat.Action getAction(
|
||||
final VideoPlayerImpl player,
|
||||
@NotificationConstants.Action final int selectedAction) {
|
||||
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
|
||||
switch (selectedAction) {
|
||||
case NotificationConstants.PREVIOUS:
|
||||
return getAction(player, baseActionIcon,
|
||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||
|
||||
case NotificationConstants.NEXT:
|
||||
return getAction(player, baseActionIcon,
|
||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||
|
||||
case NotificationConstants.REWIND:
|
||||
return getAction(player, baseActionIcon,
|
||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||
|
||||
case NotificationConstants.FORWARD:
|
||||
return getAction(player, baseActionIcon,
|
||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||
|
||||
case NotificationConstants.SMART_REWIND_PREVIOUS:
|
||||
if (player.playQueue != null && player.playQueue.size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_previous,
|
||||
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_rewind,
|
||||
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
|
||||
}
|
||||
|
||||
case NotificationConstants.SMART_FORWARD_NEXT:
|
||||
if (player.playQueue != null && player.playQueue.size() > 1) {
|
||||
return getAction(player, R.drawable.exo_notification_next,
|
||||
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_fastforward,
|
||||
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
|
||||
}
|
||||
|
||||
case NotificationConstants.PLAY_PAUSE_BUFFERING:
|
||||
if (player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
|
||||
// null intent -> show hourglass icon that does nothing when clicked
|
||||
return new NotificationCompat.Action(R.drawable.ic_hourglass_top_white_24dp_png,
|
||||
player.context.getString(R.string.notification_action_buffering),
|
||||
null);
|
||||
}
|
||||
|
||||
case NotificationConstants.PLAY_PAUSE:
|
||||
if (player.getCurrentState() == BasePlayer.STATE_COMPLETED) {
|
||||
return getAction(player, R.drawable.ic_replay_white_24dp_png,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else if (player.isPlaying()
|
||||
|| player.getCurrentState() == BasePlayer.STATE_PREFLIGHT
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BLOCKED
|
||||
|| player.getCurrentState() == BasePlayer.STATE_BUFFERING) {
|
||||
return getAction(player, R.drawable.exo_notification_pause,
|
||||
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_notification_play,
|
||||
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
|
||||
}
|
||||
|
||||
case NotificationConstants.REPEAT:
|
||||
if (player.getRepeatMode() == REPEAT_MODE_ALL) {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_all,
|
||||
R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
|
||||
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_one,
|
||||
R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
|
||||
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
|
||||
return getAction(player, R.drawable.exo_media_action_repeat_off,
|
||||
R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
|
||||
}
|
||||
|
||||
case NotificationConstants.SHUFFLE:
|
||||
if (player.playQueue != null && player.playQueue.isShuffled()) {
|
||||
return getAction(player, R.drawable.exo_controls_shuffle_on,
|
||||
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
|
||||
} else {
|
||||
return getAction(player, R.drawable.exo_controls_shuffle_off,
|
||||
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
|
||||
}
|
||||
|
||||
case NotificationConstants.CLOSE:
|
||||
return getAction(player, R.drawable.ic_close_white_24dp_png,
|
||||
R.string.close, ACTION_CLOSE);
|
||||
|
||||
case NotificationConstants.NOTHING:
|
||||
default:
|
||||
// do nothing
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationCompat.Action getAction(final VideoPlayerImpl player,
|
||||
@DrawableRes final int drawable,
|
||||
@StringRes final int title,
|
||||
final String intentAction) {
|
||||
return new NotificationCompat.Action(drawable, player.context.getString(title),
|
||||
PendingIntent.getBroadcast(player.context, NOTIFICATION_ID,
|
||||
new Intent(intentAction), FLAG_UPDATE_CURRENT));
|
||||
}
|
||||
|
||||
private Intent getIntentForNotification(final VideoPlayerImpl player) {
|
||||
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
|
||||
// Means we play in popup or audio only. Let's show the play queue
|
||||
return NavigationHelper.getPlayQueueActivityIntent(player.context);
|
||||
} else {
|
||||
// We are playing in fragment. Don't open another activity just show fragment. That's it
|
||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||
player.context, MainActivity.class, null, true);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// BITMAP
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
private void setLargeIcon(final NotificationCompat.Builder builder,
|
||||
final VideoPlayerImpl player) {
|
||||
final boolean scaleImageToSquareAspectRatio = player.sharedPreferences.getBoolean(
|
||||
player.context.getString(R.string.scale_to_square_image_in_notifications_key),
|
||||
false);
|
||||
if (scaleImageToSquareAspectRatio) {
|
||||
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
|
||||
} else {
|
||||
builder.setLargeIcon(player.getThumbnail());
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) {
|
||||
return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth());
|
||||
}
|
||||
|
||||
private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) {
|
||||
final int width = bitmap.getWidth();
|
||||
final int height = bitmap.getHeight();
|
||||
final float scaleWidth = ((float) newWidth) / width;
|
||||
final float scaleHeight = ((float) newHeight) / height;
|
||||
final Matrix matrix = new Matrix();
|
||||
matrix.postScale(scaleWidth, scaleHeight);
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
|
||||
}
|
||||
}
|
@@ -360,11 +360,11 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
return true;
|
||||
});
|
||||
// apply caption language from previous user preference
|
||||
if (userPreferredLanguage != null && (captionLanguage.equals(userPreferredLanguage)
|
||||
|| searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage)
|
||||
|| userPreferredLanguage.contains("(") && captionLanguage.startsWith(
|
||||
userPreferredLanguage
|
||||
.substring(0, userPreferredLanguage.indexOf('('))))) {
|
||||
if (userPreferredLanguage != null
|
||||
&& (captionLanguage.equals(userPreferredLanguage)
|
||||
|| (searchForAutogenerated && captionLanguage.startsWith(userPreferredLanguage))
|
||||
|| (userPreferredLanguage.contains("(") && captionLanguage.startsWith(
|
||||
userPreferredLanguage.substring(0, userPreferredLanguage.indexOf('(')))))) {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
|
@@ -55,6 +55,7 @@ import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -64,6 +65,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
@@ -108,10 +110,11 @@ import static org.schabi.newpipe.player.MainPlayer.ACTION_OPEN_CONTROLS;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
|
||||
import static org.schabi.newpipe.player.MainPlayer.NOTIFICATION_ID;
|
||||
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
@@ -134,14 +137,12 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
static final String POPUP_SAVED_WIDTH = "popup_saved_width";
|
||||
static final String POPUP_SAVED_X = "popup_saved_x";
|
||||
static final String POPUP_SAVED_Y = "popup_saved_y";
|
||||
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
|
||||
private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
||||
private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
|
||||
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
||||
|
||||
private static final float MAX_GESTURE_LENGTH = 0.75f;
|
||||
private static final int NOTIFICATION_UPDATES_BEFORE_RESET = 60;
|
||||
|
||||
private TextView titleTextView;
|
||||
private TextView channelTextView;
|
||||
@@ -187,7 +188,6 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
private boolean isVerticalVideo = false;
|
||||
private boolean fragmentIsVisible = false;
|
||||
boolean shouldUpdateOnProgress;
|
||||
int timesNotificationUpdated;
|
||||
|
||||
private final MainPlayer service;
|
||||
private PlayerServiceEventListener fragmentListener;
|
||||
@@ -198,9 +198,6 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
@NonNull
|
||||
private final AudioPlaybackResolver resolver;
|
||||
|
||||
private int cachedDuration;
|
||||
private String cachedDurationString;
|
||||
|
||||
// Popup
|
||||
private WindowManager.LayoutParams popupLayoutParams;
|
||||
public WindowManager windowManager;
|
||||
@@ -312,6 +309,9 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
|
||||
titleTextView.setSelected(true);
|
||||
channelTextView.setSelected(true);
|
||||
|
||||
// Prevent hiding of bottom sheet via swipe inside queue
|
||||
this.itemsList.setNestedScrollingEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -578,29 +578,32 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
setupScreenRotationButton();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ExoPlayer Video Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ExoPlayer Video Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
void onShuffleOrRepeatModeChanged() {
|
||||
updatePlaybackButtons();
|
||||
updatePlayback();
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(final int i) {
|
||||
super.onRepeatModeChanged(i);
|
||||
updatePlaybackButtons();
|
||||
updatePlayback();
|
||||
service.resetNotification();
|
||||
service.updateNotification(-1);
|
||||
onShuffleOrRepeatModeChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleClicked() {
|
||||
super.onShuffleClicked();
|
||||
updatePlaybackButtons();
|
||||
updatePlayback();
|
||||
onShuffleOrRepeatModeChanged();
|
||||
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onPlayerError(final ExoPlaybackException error) {
|
||||
@@ -611,6 +614,13 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(final Timeline timeline, final int reason) {
|
||||
super.onTimelineChanged(timeline, reason);
|
||||
// force recreate notification to ensure seek bar is shown when preparation finishes
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
|
||||
}
|
||||
|
||||
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||
super.onMetadataChanged(tag);
|
||||
|
||||
@@ -619,8 +629,7 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
titleTextView.setText(tag.getMetadata().getName());
|
||||
channelTextView.setText(tag.getMetadata().getUploaderName());
|
||||
|
||||
service.resetNotification();
|
||||
service.updateNotification(-1);
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
updateMetadata();
|
||||
}
|
||||
|
||||
@@ -643,35 +652,17 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
public void onUpdateProgress(final int currentProgress,
|
||||
final int duration, final int bufferPercent) {
|
||||
super.onUpdateProgress(currentProgress, duration, bufferPercent);
|
||||
|
||||
updateProgress(currentProgress, duration, bufferPercent);
|
||||
|
||||
if (!shouldUpdateOnProgress || getCurrentState() == BasePlayer.STATE_COMPLETED
|
||||
|| getCurrentState() == BasePlayer.STATE_PAUSED || getPlayQueue() == null) {
|
||||
return;
|
||||
}
|
||||
// setMetadata only updates the metadata when any of the metadata keys are null
|
||||
mediaSessionManager.setMetadata(getVideoTitle(), getUploaderName(), getThumbnail(),
|
||||
duration);
|
||||
}
|
||||
|
||||
if (timesNotificationUpdated > NOTIFICATION_UPDATES_BEFORE_RESET) {
|
||||
service.resetNotification();
|
||||
}
|
||||
|
||||
if (service.getBigNotRemoteView() != null) {
|
||||
if (cachedDuration != duration) {
|
||||
cachedDuration = duration;
|
||||
cachedDurationString = getTimeString(duration);
|
||||
}
|
||||
service.getBigNotRemoteView()
|
||||
.setProgressBar(R.id.notificationProgressBar,
|
||||
duration, currentProgress, false);
|
||||
service.getBigNotRemoteView()
|
||||
.setTextViewText(R.id.notificationTime,
|
||||
getTimeString(currentProgress) + " / " + cachedDurationString);
|
||||
}
|
||||
if (service.getNotRemoteView() != null) {
|
||||
service.getNotRemoteView()
|
||||
.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
|
||||
}
|
||||
service.updateNotification(-1);
|
||||
@Override
|
||||
public void onPlayQueueEdited() {
|
||||
updatePlayback();
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -709,62 +700,33 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player Overrides
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
// Player Overrides
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void toggleFullscreen() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "toggleFullscreen() called");
|
||||
}
|
||||
if (simpleExoPlayer == null || getCurrentMetadata() == null) {
|
||||
if (popupPlayerSelected()
|
||||
|| simpleExoPlayer == null
|
||||
|| getCurrentMetadata() == null
|
||||
|| fragmentListener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (popupPlayerSelected()) {
|
||||
setRecovery();
|
||||
service.removeViewFromParent();
|
||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||
service,
|
||||
MainActivity.class,
|
||||
this.getPlayQueue(),
|
||||
this.getRepeatMode(),
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
null,
|
||||
true,
|
||||
!isPlaying(),
|
||||
isMuted()
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Constants.KEY_SERVICE_ID,
|
||||
getCurrentMetadata().getMetadata().getServiceId());
|
||||
intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
|
||||
intent.putExtra(Constants.KEY_URL, getVideoUrl());
|
||||
intent.putExtra(Constants.KEY_TITLE, getVideoTitle());
|
||||
intent.putExtra(VideoDetailFragment.AUTO_PLAY, true);
|
||||
service.onDestroy();
|
||||
context.startActivity(intent);
|
||||
return;
|
||||
isFullscreen = !isFullscreen;
|
||||
if (!isFullscreen) {
|
||||
// Apply window insets because Android will not do it when orientation changes
|
||||
// from landscape to portrait (open vertical video to reproduce)
|
||||
getControlsRoot().setPadding(0, 0, 0, 0);
|
||||
} else {
|
||||
if (fragmentListener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isFullscreen = !isFullscreen;
|
||||
if (!isFullscreen) {
|
||||
// Apply window insets because Android will not do it when orientation changes
|
||||
// from landscape to portrait (open vertical video to reproduce)
|
||||
getControlsRoot().setPadding(0, 0, 0, 0);
|
||||
} else {
|
||||
// Android needs tens milliseconds to send new insets but a user is able to see
|
||||
// how controls changes it's position from `0` to `nav bar height` padding.
|
||||
// So just hide the controls to hide this visual inconsistency
|
||||
hideControls(0, 0);
|
||||
}
|
||||
fragmentListener.onFullscreenStateChanged(isFullscreen());
|
||||
// Android needs tens milliseconds to send new insets but a user is able to see
|
||||
// how controls changes it's position from `0` to `nav bar height` padding.
|
||||
// So just hide the controls to hide this visual inconsistency
|
||||
hideControls(0, 0);
|
||||
}
|
||||
fragmentListener.onFullscreenStateChanged(isFullscreen());
|
||||
|
||||
if (!isFullscreen()) {
|
||||
titleTextView.setVisibility(View.GONE);
|
||||
@@ -778,6 +740,40 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
setupScreenRotationButton();
|
||||
}
|
||||
|
||||
public void switchFromPopupToMain() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "switchFromPopupToMain() called");
|
||||
}
|
||||
if (!popupPlayerSelected() || simpleExoPlayer == null || getCurrentMetadata() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRecovery();
|
||||
service.removeViewFromParent();
|
||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||
service,
|
||||
MainActivity.class,
|
||||
this.getPlayQueue(),
|
||||
this.getRepeatMode(),
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
null,
|
||||
true,
|
||||
!isPlaying(),
|
||||
isMuted()
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Constants.KEY_SERVICE_ID,
|
||||
getCurrentMetadata().getMetadata().getServiceId());
|
||||
intent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
|
||||
intent.putExtra(Constants.KEY_URL, getVideoUrl());
|
||||
intent.putExtra(Constants.KEY_TITLE, getVideoTitle());
|
||||
intent.putExtra(VideoDetailFragment.AUTO_PLAY, true);
|
||||
service.onDestroy();
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
super.onClick(v);
|
||||
@@ -805,9 +801,12 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
} else if (v.getId() == openInBrowser.getId()) {
|
||||
onOpenInBrowserClicked();
|
||||
} else if (v.getId() == fullscreenButton.getId()) {
|
||||
toggleFullscreen();
|
||||
switchFromPopupToMain();
|
||||
} else if (v.getId() == screenRotationButton.getId()) {
|
||||
if (!isVerticalVideo) {
|
||||
// Only if it's not a vertical video or vertical video but in landscape with locked
|
||||
// orientation a screen orientation can be changed automatically
|
||||
if (!isVerticalVideo
|
||||
|| (service.isLandscape() && globalScreenOrientationLocked(service))) {
|
||||
fragmentListener.onScreenRotationButtonClicked();
|
||||
} else {
|
||||
toggleFullscreen();
|
||||
@@ -823,7 +822,9 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
showHideShadow(true, DEFAULT_CONTROLS_DURATION, 0);
|
||||
animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> {
|
||||
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
|
||||
if (v.getId() == playPauseButton.getId()) {
|
||||
if (v.getId() == playPauseButton.getId()
|
||||
// Hide controls in fullscreen immediately
|
||||
|| (v.getId() == screenRotationButton.getId() && isFullscreen)) {
|
||||
hideControls(0, 0);
|
||||
} else {
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
@@ -942,9 +943,8 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
|
||||
private void setupScreenRotationButton() {
|
||||
final boolean orientationLocked = PlayerHelper.globalScreenOrientationLocked(service);
|
||||
final boolean tabletInLandscape = DeviceUtils.isTablet(service) && service.isLandscape();
|
||||
final boolean showButton = videoPlayerSelected()
|
||||
&& (orientationLocked || isVerticalVideo || tabletInLandscape);
|
||||
&& (orientationLocked || isVerticalVideo || DeviceUtils.isTablet(service));
|
||||
screenRotationButton.setVisibility(showButton ? View.VISIBLE : View.GONE);
|
||||
screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(service, isFullscreen()
|
||||
? R.drawable.ic_fullscreen_exit_white_24dp
|
||||
@@ -956,6 +956,8 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
if (orientationLocked
|
||||
&& isFullscreen()
|
||||
&& service.isLandscape() == isVerticalVideo
|
||||
&& !DeviceUtils.isTv(service)
|
||||
&& !DeviceUtils.isTablet(service)
|
||||
&& fragmentListener != null) {
|
||||
fragmentListener.onScreenRotationButtonClicked();
|
||||
}
|
||||
@@ -986,6 +988,7 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
super.onDismiss(menu);
|
||||
if (isPlaying()) {
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||
hideSystemUIIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,15 +1013,6 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
|
||||
setInitialGestureValues();
|
||||
queueLayout.getLayoutParams().height = height - queueLayout.getTop();
|
||||
|
||||
if (popupPlayerSelected()) {
|
||||
final float widthDp = Math.abs(r - l) / service.getResources()
|
||||
.getDisplayMetrics().density;
|
||||
final int visibility = widthDp > MINIMUM_SHOW_EXTRA_WIDTH_DP
|
||||
? View.VISIBLE
|
||||
: View.GONE;
|
||||
secondaryControls.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,8 +1095,7 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
animatePlayButtons(false, 100);
|
||||
getRootView().setKeepScreenOn(false);
|
||||
|
||||
service.resetNotification();
|
||||
service.updateNotification(R.drawable.exo_controls_play);
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1110,8 +1103,9 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
super.onBuffering();
|
||||
getRootView().setKeepScreenOn(true);
|
||||
|
||||
service.resetNotification();
|
||||
service.updateNotification(R.drawable.exo_controls_play);
|
||||
if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) {
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1129,10 +1123,7 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
checkLandscape();
|
||||
getRootView().setKeepScreenOn(true);
|
||||
|
||||
service.resetNotification();
|
||||
service.updateNotification(R.drawable.exo_controls_pause);
|
||||
|
||||
service.startForeground(NOTIFICATION_ID, service.getNotBuilder().build());
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1148,13 +1139,12 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
|
||||
updateWindowFlags(IDLE_WINDOW_FLAGS);
|
||||
|
||||
service.resetNotification();
|
||||
service.updateNotification(R.drawable.exo_controls_play);
|
||||
|
||||
// Remove running notification when user don't want music (or video in popup)
|
||||
// to be played in background
|
||||
if (!minimizeOnPopupEnabled() && !backgroundPlaybackEnabled() && videoPlayerSelected()) {
|
||||
service.stopForeground(true);
|
||||
NotificationUtil.getInstance().cancelNotificationAndStopForeground(service);
|
||||
} else {
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
getRootView().setKeepScreenOn(false);
|
||||
@@ -1166,8 +1156,7 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
animatePlayButtons(false, 100);
|
||||
getRootView().setKeepScreenOn(true);
|
||||
|
||||
service.resetNotification();
|
||||
service.updateNotification(R.drawable.exo_controls_play);
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1177,20 +1166,20 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay_white_24dp);
|
||||
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
getRootView().setKeepScreenOn(false);
|
||||
|
||||
getRootView().setKeepScreenOn(false);
|
||||
updateWindowFlags(IDLE_WINDOW_FLAGS);
|
||||
|
||||
service.resetNotification();
|
||||
service.updateNotification(R.drawable.ic_replay_white_24dp);
|
||||
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
if (isFullscreen) {
|
||||
toggleFullscreen();
|
||||
}
|
||||
super.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
||||
service.getContentResolver().unregisterContentObserver(settingsContentObserver);
|
||||
}
|
||||
|
||||
@@ -1214,6 +1203,8 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
intentFilter.addAction(ACTION_PLAY_NEXT);
|
||||
intentFilter.addAction(ACTION_FAST_REWIND);
|
||||
intentFilter.addAction(ACTION_FAST_FORWARD);
|
||||
intentFilter.addAction(ACTION_SHUFFLE);
|
||||
intentFilter.addAction(ACTION_RECREATE_NOTIFICATION);
|
||||
|
||||
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED);
|
||||
intentFilter.addAction(VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED);
|
||||
@@ -1263,6 +1254,17 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
case ACTION_REPEAT:
|
||||
onRepeatClicked();
|
||||
break;
|
||||
case ACTION_SHUFFLE:
|
||||
onShuffleClicked();
|
||||
break;
|
||||
case ACTION_RECREATE_NOTIFICATION:
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
|
||||
break;
|
||||
case Intent.ACTION_HEADSET_PLUG: //FIXME
|
||||
/*notificationManager.cancel(NOTIFICATION_ID);
|
||||
mediaSessionManager.dispose();
|
||||
mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
|
||||
break;
|
||||
case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED:
|
||||
fragmentIsVisible = true;
|
||||
useVideoSource(true);
|
||||
@@ -1302,7 +1304,6 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
}
|
||||
break;
|
||||
}
|
||||
service.resetNotification();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1314,10 +1315,7 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
final View view,
|
||||
final Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
// rebuild notification here since remote view does not release bitmaps,
|
||||
// causing memory leaks
|
||||
service.resetNotification();
|
||||
service.updateNotification(-1);
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1325,20 +1323,18 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
final View view,
|
||||
final FailReason failReason) {
|
||||
super.onLoadingFailed(imageUri, view, failReason);
|
||||
service.resetNotification();
|
||||
service.updateNotification(-1);
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(final String imageUri, final View view) {
|
||||
super.onLoadingCancelled(imageUri, view);
|
||||
service.resetNotification();
|
||||
service.updateNotification(-1);
|
||||
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setInitialGestureValues() {
|
||||
if (getAudioReactor() != null) {
|
||||
@@ -1492,6 +1488,10 @@ public class VideoPlayerImpl extends VideoPlayer
|
||||
}
|
||||
}
|
||||
|
||||
public void disablePreloadingOfCurrentTrack() {
|
||||
getLoadController().disablePreloadingOfCurrentTrack();
|
||||
}
|
||||
|
||||
protected void setMuteButton(final ImageButton button, final boolean isMuted) {
|
||||
button.setImageDrawable(AppCompatResources.getDrawable(service, isMuted
|
||||
? R.drawable.ic_volume_off_white_24dp : R.drawable.ic_volume_up_white_24dp));
|
||||
|
@@ -13,6 +13,7 @@ public class LoadController implements LoadControl {
|
||||
|
||||
private final long initialPlaybackBufferUs;
|
||||
private final LoadControl internalLoadControl;
|
||||
private boolean preloadingEnabled = true;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Default Load Control
|
||||
@@ -41,6 +42,7 @@ public class LoadController implements LoadControl {
|
||||
|
||||
@Override
|
||||
public void onPrepared() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onPrepared();
|
||||
}
|
||||
|
||||
@@ -52,11 +54,13 @@ public class LoadController implements LoadControl {
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onStopped();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReleased() {
|
||||
preloadingEnabled = true;
|
||||
internalLoadControl.onReleased();
|
||||
}
|
||||
|
||||
@@ -78,6 +82,9 @@ public class LoadController implements LoadControl {
|
||||
@Override
|
||||
public boolean shouldContinueLoading(final long bufferedDurationUs,
|
||||
final float playbackSpeed) {
|
||||
if (!preloadingEnabled) {
|
||||
return false;
|
||||
}
|
||||
return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
|
||||
}
|
||||
|
||||
@@ -90,4 +97,8 @@ public class LoadController implements LoadControl {
|
||||
.shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering);
|
||||
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
|
||||
}
|
||||
|
||||
public void disablePreloadingOfCurrentTrack() {
|
||||
preloadingEnabled = false;
|
||||
}
|
||||
}
|
||||
|
@@ -3,44 +3,58 @@ package org.schabi.newpipe.player.helper;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.MediaMetadata;
|
||||
import android.os.Build;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.media.app.NotificationCompat.MediaStyle;
|
||||
import androidx.media.session.MediaButtonReceiver;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = "MediaSessionManager";
|
||||
private static final String TAG = MediaSessionManager.class.getSimpleName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
@NonNull
|
||||
private final MediaSessionCompat mediaSession;
|
||||
@NonNull
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
|
||||
private int lastAlbumArtHashCode;
|
||||
|
||||
public MediaSessionManager(@NonNull final Context context,
|
||||
@NonNull final Player player,
|
||||
@NonNull final MediaSessionCallback callback) {
|
||||
this.mediaSession = new MediaSessionCompat(context, TAG);
|
||||
this.mediaSession.setActive(true);
|
||||
mediaSession = new MediaSessionCompat(context, TAG);
|
||||
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
|
||||
| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
|
||||
mediaSession.setActive(true);
|
||||
|
||||
this.sessionConnector = new MediaSessionConnector(mediaSession);
|
||||
this.sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback));
|
||||
this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
|
||||
this.sessionConnector.setPlayer(player);
|
||||
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
|
||||
.setState(PlaybackStateCompat.STATE_NONE, -1, 1)
|
||||
.setActions(PlaybackStateCompat.ACTION_SEEK_TO
|
||||
| PlaybackStateCompat.ACTION_PLAY
|
||||
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
|
||||
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
|
||||
| PlaybackStateCompat.ACTION_STOP)
|
||||
.build());
|
||||
|
||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
||||
sessionConnector.setControlDispatcher(new PlayQueuePlaybackController(callback));
|
||||
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
|
||||
sessionConnector.setPlayer(player);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -49,46 +63,78 @@ public class MediaSessionManager {
|
||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public void setLockScreenArt(final NotificationCompat.Builder builder,
|
||||
@Nullable final Bitmap thumbnailBitmap) {
|
||||
if (thumbnailBitmap == null || !mediaSession.isActive()) {
|
||||
public MediaSessionCompat.Token getSessionToken() {
|
||||
return mediaSession.getSessionToken();
|
||||
}
|
||||
|
||||
public void setMetadata(final String title,
|
||||
final String artist,
|
||||
final Bitmap albumArt,
|
||||
final long duration) {
|
||||
if (albumArt == null || !mediaSession.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mediaSession.setMetadata(
|
||||
new MediaMetadataCompat.Builder()
|
||||
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, thumbnailBitmap)
|
||||
.build()
|
||||
);
|
||||
if (DEBUG) {
|
||||
if (getMetadataAlbumArt() == null) {
|
||||
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
|
||||
}
|
||||
if (getMetadataTitle() == null) {
|
||||
Log.d(TAG, "N_getMetadataTitle: title == null");
|
||||
}
|
||||
if (getMetadataArtist() == null) {
|
||||
Log.d(TAG, "N_getMetadataArtist: artist == null");
|
||||
}
|
||||
if (getMetadataDuration() <= 1) {
|
||||
Log.d(TAG, "N_getMetadataDuration: duration <= 1; " + getMetadataDuration());
|
||||
}
|
||||
}
|
||||
|
||||
final MediaStyle mediaStyle = new MediaStyle()
|
||||
.setMediaSession(mediaSession.getSessionToken());
|
||||
if (getMetadataAlbumArt() == null || getMetadataTitle() == null
|
||||
|| getMetadataArtist() == null || getMetadataDuration() <= 1
|
||||
|| albumArt.hashCode() != lastAlbumArtHashCode) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "setMetadata: N_Metadata update: t: " + title + " a: " + artist
|
||||
+ " thumb: " + albumArt.hashCode() + " d: " + duration);
|
||||
}
|
||||
|
||||
builder.setStyle(mediaStyle);
|
||||
mediaSession.setMetadata(new MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, albumArt)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration).build());
|
||||
lastAlbumArtHashCode = albumArt.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public void clearLockScreenArt(final NotificationCompat.Builder builder) {
|
||||
mediaSession.setMetadata(
|
||||
new MediaMetadataCompat.Builder()
|
||||
.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, null)
|
||||
.build()
|
||||
);
|
||||
private Bitmap getMetadataAlbumArt() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
|
||||
}
|
||||
|
||||
final MediaStyle mediaStyle = new MediaStyle()
|
||||
.setMediaSession(mediaSession.getSessionToken());
|
||||
private String getMetadataTitle() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
|
||||
}
|
||||
|
||||
builder.setStyle(mediaStyle);
|
||||
private String getMetadataArtist() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
|
||||
}
|
||||
|
||||
private long getMetadataDuration() {
|
||||
return mediaSession.getController().getMetadata()
|
||||
.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on player destruction to prevent leakage.
|
||||
*/
|
||||
public void dispose() {
|
||||
this.sessionConnector.setPlayer(null);
|
||||
this.sessionConnector.setQueueNavigator(null);
|
||||
this.mediaSession.setActive(false);
|
||||
this.mediaSession.release();
|
||||
sessionConnector.setPlayer(null);
|
||||
sessionConnector.setQueueNavigator(null);
|
||||
mediaSession.setActive(false);
|
||||
mediaSession.release();
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.text.DecimalFormat;
|
||||
@@ -248,6 +249,18 @@ public final class PlayerHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAutoplayAllowedByUser(@NonNull final Context context) {
|
||||
switch (PlayerHelper.getAutoplayType(context)) {
|
||||
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER:
|
||||
return false;
|
||||
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI:
|
||||
return !ListHelper.isMeteredNetwork(context);
|
||||
case PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SeekParameters getSeekParameters(@NonNull final Context context) {
|
||||
return isUsingInexactSeek(context) ? SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
|
||||
|
@@ -255,21 +255,21 @@ public class MediaSourceManager {
|
||||
|
||||
// Loading and Syncing
|
||||
switch (event.type()) {
|
||||
case INIT:
|
||||
case REORDER:
|
||||
case ERROR:
|
||||
case SELECT:
|
||||
case INIT: case REORDER: case ERROR: case SELECT:
|
||||
loadImmediate(); // low frequency, critical events
|
||||
break;
|
||||
case APPEND:
|
||||
case REMOVE:
|
||||
case MOVE:
|
||||
case RECOVERY:
|
||||
case APPEND: case REMOVE: case MOVE: case RECOVERY:
|
||||
default:
|
||||
loadDebounced(); // high frequency or noncritical events
|
||||
break;
|
||||
}
|
||||
|
||||
// update ui and notification
|
||||
switch (event.type()) {
|
||||
case APPEND: case REMOVE: case MOVE: case REORDER:
|
||||
playbackListener.onPlayQueueEdited();
|
||||
}
|
||||
|
||||
if (!isPlayQueueReady()) {
|
||||
maybeBlock();
|
||||
playQueue.fetch();
|
||||
|
@@ -69,7 +69,7 @@ public interface PlaybackListener {
|
||||
MediaSource sourceOf(PlayQueueItem item, StreamInfo info);
|
||||
|
||||
/**
|
||||
* Called when the play queue can no longer to played or used.
|
||||
* Called when the play queue can no longer be played or used.
|
||||
* Currently, this means the play queue is empty and complete.
|
||||
* Signals to the listener that it should shutdown.
|
||||
* <p>
|
||||
@@ -77,4 +77,13 @@ public interface PlaybackListener {
|
||||
* </p>
|
||||
*/
|
||||
void onPlaybackShutdown();
|
||||
|
||||
/**
|
||||
* Called whenever the play queue was edited (items were added, deleted or moved),
|
||||
* use this to e.g. update notification buttons or fragment ui.
|
||||
* <p>
|
||||
* May be called at any time.
|
||||
* </p>
|
||||
*/
|
||||
void onPlayQueueEdited();
|
||||
}
|
||||
|
@@ -20,7 +20,8 @@ public enum UserAction {
|
||||
DELETE_FROM_HISTORY("delete from history"),
|
||||
PLAY_STREAM("Play stream"),
|
||||
DOWNLOAD_POSTPROCESSING("download post-processing"),
|
||||
DOWNLOAD_FAILED("download failed");
|
||||
DOWNLOAD_FAILED("download failed"),
|
||||
PREFERENCES_MIGRATION("migration of preferences");
|
||||
|
||||
|
||||
private final String message;
|
||||
|
@@ -5,12 +5,12 @@ import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
@@ -25,24 +25,16 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
public void onViewCreated(@NonNull final View rootView,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
setDivider(null);
|
||||
updateTitle();
|
||||
ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
if (getActivity() instanceof AppCompatActivity) {
|
||||
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(getPreferenceScreen().getTitle());
|
||||
}
|
||||
}
|
||||
ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Set;
|
||||
|
||||
/*
|
||||
* Created by k3b on 07.01.2016.
|
||||
@@ -38,6 +39,22 @@ public final class NewPipeSettings {
|
||||
private NewPipeSettings() { }
|
||||
|
||||
public static void initSettings(final Context context) {
|
||||
// check if there are entries in the prefs to determine whether this is the first app run
|
||||
Boolean isFirstRun = null;
|
||||
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getAll().keySet();
|
||||
for (final String key: prefsKeys) {
|
||||
// ACRA stores some info in the prefs during app initialization
|
||||
// which happens before this method is called. Therefore ignore ACRA-related keys.
|
||||
if (!key.toLowerCase().startsWith("acra")) {
|
||||
isFirstRun = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isFirstRun == null) {
|
||||
isFirstRun = true;
|
||||
}
|
||||
|
||||
PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.content_settings, true);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
|
||||
@@ -48,6 +65,8 @@ public final class NewPipeSettings {
|
||||
|
||||
getVideoDownloadFolder(context);
|
||||
getAudioDownloadFolder(context);
|
||||
|
||||
SettingMigrations.initMigrations(context, isFirstRun);
|
||||
}
|
||||
|
||||
private static void getVideoDownloadFolder(final Context context) {
|
||||
|
@@ -0,0 +1,270 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.NotificationConstants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class NotificationSettingsFragment extends Fragment {
|
||||
|
||||
private Switch scaleSwitch;
|
||||
private NotificationSlot[] notificationSlots;
|
||||
|
||||
private SharedPreferences pref;
|
||||
private List<Integer> compactSlots;
|
||||
private String scaleKey;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
pref = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
scaleKey = getString(R.string.scale_to_square_image_in_notifications_key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.settings_notification, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
|
||||
setupScaleSwitch(rootView);
|
||||
setupActions(rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
|
||||
getString(R.string.settings_category_notification_title));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
saveChanges();
|
||||
requireContext().sendBroadcast(new Intent(MainPlayer.ACTION_RECREATE_NOTIFICATION));
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Setup
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void setupScaleSwitch(@NonNull final View view) {
|
||||
scaleSwitch = view.findViewById(R.id.notificationScaleSwitch);
|
||||
scaleSwitch.setChecked(pref.getBoolean(scaleKey, false));
|
||||
|
||||
view.findViewById(R.id.notificationScaleSwitchClickableArea)
|
||||
.setOnClickListener(v -> scaleSwitch.toggle());
|
||||
}
|
||||
|
||||
private void setupActions(@NonNull final View view) {
|
||||
compactSlots =
|
||||
NotificationConstants.getCompactSlotsFromPreferences(requireContext(), pref, 5);
|
||||
notificationSlots = new NotificationSlot[5];
|
||||
for (int i = 0; i < 5; i++) {
|
||||
notificationSlots[i] = new NotificationSlot(i, view);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Saving
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void saveChanges() {
|
||||
final SharedPreferences.Editor editor = pref.edit();
|
||||
editor.putBoolean(scaleKey, scaleSwitch.isChecked());
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
editor.putInt(getString(NotificationConstants.SLOT_COMPACT_PREF_KEYS[i]),
|
||||
(i < compactSlots.size() ? compactSlots.get(i) : -1));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
editor.putInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||
notificationSlots[i].selectedAction);
|
||||
}
|
||||
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Notification action
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static final int[] SLOT_ITEMS = {
|
||||
R.id.notificationAction0,
|
||||
R.id.notificationAction1,
|
||||
R.id.notificationAction2,
|
||||
R.id.notificationAction3,
|
||||
R.id.notificationAction4,
|
||||
};
|
||||
|
||||
private static final int[] SLOT_TITLES = {
|
||||
R.string.notification_action_0_title,
|
||||
R.string.notification_action_1_title,
|
||||
R.string.notification_action_2_title,
|
||||
R.string.notification_action_3_title,
|
||||
R.string.notification_action_4_title,
|
||||
};
|
||||
|
||||
private class NotificationSlot {
|
||||
|
||||
final int i;
|
||||
@NotificationConstants.Action int selectedAction;
|
||||
|
||||
ImageView icon;
|
||||
TextView summary;
|
||||
|
||||
NotificationSlot(final int actionIndex, final View parentView) {
|
||||
this.i = actionIndex;
|
||||
|
||||
final View view = parentView.findViewById(SLOT_ITEMS[i]);
|
||||
setupSelectedAction(view);
|
||||
setupTitle(view);
|
||||
setupCheckbox(view);
|
||||
}
|
||||
|
||||
void setupTitle(final View view) {
|
||||
((TextView) view.findViewById(R.id.notificationActionTitle))
|
||||
.setText(SLOT_TITLES[i]);
|
||||
view.findViewById(R.id.notificationActionClickableArea).setOnClickListener(
|
||||
v -> openActionChooserDialog());
|
||||
}
|
||||
|
||||
void setupCheckbox(final View view) {
|
||||
final CheckBox compactSlotCheckBox = view.findViewById(R.id.notificationActionCheckBox);
|
||||
compactSlotCheckBox.setChecked(compactSlots.contains(i));
|
||||
view.findViewById(R.id.notificationActionCheckBoxClickableArea).setOnClickListener(
|
||||
v -> {
|
||||
if (compactSlotCheckBox.isChecked()) {
|
||||
compactSlots.remove((Integer) i);
|
||||
} else if (compactSlots.size() < 3) {
|
||||
compactSlots.add(i);
|
||||
} else {
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.notification_actions_at_most_three,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
compactSlotCheckBox.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
void setupSelectedAction(final View view) {
|
||||
icon = view.findViewById(R.id.notificationActionIcon);
|
||||
summary = view.findViewById(R.id.notificationActionSummary);
|
||||
selectedAction = pref.getInt(getString(NotificationConstants.SLOT_PREF_KEYS[i]),
|
||||
NotificationConstants.SLOT_DEFAULTS[i]);
|
||||
updateInfo();
|
||||
}
|
||||
|
||||
void updateInfo() {
|
||||
if (NotificationConstants.ACTION_ICONS[selectedAction] == 0) {
|
||||
icon.setImageDrawable(null);
|
||||
} else {
|
||||
icon.setImageDrawable(AppCompatResources.getDrawable(requireContext(),
|
||||
NotificationConstants.ACTION_ICONS[selectedAction]));
|
||||
}
|
||||
|
||||
summary.setText(NotificationConstants.getActionName(requireContext(), selectedAction));
|
||||
}
|
||||
|
||||
void openActionChooserDialog() {
|
||||
final LayoutInflater inflater = LayoutInflater.from(requireContext());
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
|
||||
R.layout.single_choice_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(requireContext())
|
||||
.setTitle(SLOT_TITLES[i])
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
|
||||
final View.OnClickListener radioButtonsClickListener = v -> {
|
||||
selectedAction = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][v.getId()];
|
||||
updateInfo();
|
||||
alertDialog.dismiss();
|
||||
};
|
||||
|
||||
for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) {
|
||||
final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id];
|
||||
final RadioButton radioButton
|
||||
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||
|
||||
// if present set action icon with correct color
|
||||
if (NotificationConstants.ACTION_ICONS[action] != 0) {
|
||||
final Drawable drawable = AppCompatResources.getDrawable(requireContext(),
|
||||
NotificationConstants.ACTION_ICONS[action]);
|
||||
if (drawable != null) {
|
||||
final int color = ThemeHelper.resolveColorFromAttr(requireContext(),
|
||||
android.R.attr.textColorPrimary);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
drawable.setTint(color);
|
||||
} else {
|
||||
drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
radioButton.setCompoundDrawablesWithIntrinsicBounds(
|
||||
null, null, drawable, null);
|
||||
}
|
||||
}
|
||||
|
||||
radioButton.setText(NotificationConstants.getActionName(requireContext(), action));
|
||||
radioButton.setChecked(action == selectedAction);
|
||||
radioButton.setId(id);
|
||||
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
radioButton.setOnClickListener(radioButtonsClickListener);
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
alertDialog.show();
|
||||
|
||||
if (DeviceUtils.isTv(requireContext())) {
|
||||
FocusOverlayView.setupFocusObserver(alertDialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,9 +22,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -117,7 +115,8 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateTitle();
|
||||
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
|
||||
getString(R.string.peertube_instance_url_title));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -176,15 +175,6 @@ public class PeertubeInstanceListFragment extends Fragment {
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
if (getActivity() instanceof AppCompatActivity) {
|
||||
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(R.string.peertube_instance_url_title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChanges() {
|
||||
final JsonStringWriter jsonWriter = JsonWriter.string().object().array("instances");
|
||||
for (final PeertubeInstance instance : instanceList) {
|
||||
|
@@ -0,0 +1,140 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public final class SettingMigrations {
|
||||
private static final String TAG = SettingMigrations.class.toString();
|
||||
/**
|
||||
* Version number for preferences. Must be incremented every time a migration is necessary.
|
||||
*/
|
||||
public static final int VERSION = 2;
|
||||
private static SharedPreferences sp;
|
||||
|
||||
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
|
||||
@Override
|
||||
public void migrate(final Context context) {
|
||||
// We changed the content of the dialog which opens when sharing a link to NewPipe
|
||||
// by removing the "open detail page" option.
|
||||
// Therefore, show the dialog once again to ensure users need to choose again and are
|
||||
// aware of the changed dialog.
|
||||
final SharedPreferences.Editor editor = sp.edit();
|
||||
editor.putString(context.getString(R.string.preferred_open_action_key),
|
||||
context.getString(R.string.always_ask_open_action_key));
|
||||
editor.apply();
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
|
||||
@Override
|
||||
protected void migrate(final Context context) {
|
||||
// The new application workflow introduced in #2907 allows minimizing videos
|
||||
// while playing to do other stuff within the app.
|
||||
// For an even better workflow, we minimize a stream when switching the app to play in
|
||||
// background.
|
||||
// Therefore, set default value to background, if it has not been changed yet.
|
||||
final String minimizeOnExitKey = context.getString(R.string.minimize_on_exit_key);
|
||||
if (sp.getString(minimizeOnExitKey, "")
|
||||
.equals(context.getString(R.string.minimize_on_exit_none_key))) {
|
||||
final SharedPreferences.Editor editor = sp.edit();
|
||||
editor.putString(minimizeOnExitKey,
|
||||
context.getString(R.string.minimize_on_exit_background_key));
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* List of all implemented migrations.
|
||||
* <p>
|
||||
* <b>Append new migrations to the end of the list</b> to keep it sorted ascending.
|
||||
* If not sorted correctly, migrations which depend on each other, may fail.
|
||||
*/
|
||||
private static final Migration[] SETTING_MIGRATIONS = {
|
||||
MIGRATION_0_1,
|
||||
MIGRATION_1_2
|
||||
};
|
||||
|
||||
|
||||
public static void initMigrations(final Context context, final boolean isFirstRun) {
|
||||
// setup migrations and check if there is something to do
|
||||
sp = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
|
||||
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
|
||||
|
||||
// no migration to run, already up to date
|
||||
if (isFirstRun) {
|
||||
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
|
||||
return;
|
||||
} else if (lastPrefVersion == VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
// run migrations
|
||||
int currentVersion = lastPrefVersion;
|
||||
for (final Migration currentMigration : SETTING_MIGRATIONS) {
|
||||
try {
|
||||
if (currentMigration.shouldMigrate(currentVersion)) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Migrating preferences from version "
|
||||
+ currentVersion + " to " + currentMigration.newVersion);
|
||||
}
|
||||
currentMigration.migrate(context);
|
||||
currentVersion = currentMigration.newVersion;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// save the version with the last successful migration and report the error
|
||||
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
|
||||
final ErrorInfo errorInfo = ErrorInfo.make(
|
||||
UserAction.PREFERENCES_MIGRATION,
|
||||
"none",
|
||||
"Migrating preferences from version " + lastPrefVersion + " to "
|
||||
+ VERSION + ". "
|
||||
+ "Error at " + currentVersion + " => " + ++currentVersion,
|
||||
0
|
||||
);
|
||||
ErrorActivity.reportError(context, e, SettingMigrations.class, null, errorInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// store the current preferences version
|
||||
sp.edit().putInt(lastPrefVersionKey, currentVersion).apply();
|
||||
}
|
||||
|
||||
private SettingMigrations() { }
|
||||
|
||||
abstract static class Migration {
|
||||
public final int oldVersion;
|
||||
public final int newVersion;
|
||||
|
||||
protected Migration(final int oldVersion, final int newVersion) {
|
||||
this.oldVersion = oldVersion;
|
||||
this.newVersion = newVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currentVersion current settings version
|
||||
* @return Returns whether this migration should be run.
|
||||
* A migration is necessary if the old version of this migration is lower than or equal to
|
||||
* the current settings version.
|
||||
*/
|
||||
private boolean shouldMigrate(final int currentVersion) {
|
||||
return oldVersion >= currentVersion;
|
||||
}
|
||||
|
||||
protected abstract void migrate(Context context);
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -16,9 +16,7 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -92,7 +90,8 @@ public class ChooseTabsFragment extends Fragment {
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateTitle();
|
||||
ThemeHelper.setTitleToAppCompatActivity(getActivity(),
|
||||
getString(R.string.main_page_content));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -137,15 +136,6 @@ public class ChooseTabsFragment extends Fragment {
|
||||
tabList.addAll(tabsManager.getTabs());
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
if (getActivity() instanceof AppCompatActivity) {
|
||||
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(R.string.main_page_content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChanges() {
|
||||
tabsManager.saveTabs(tabList);
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ import org.schabi.newpipe.settings.SettingsActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
@SuppressWarnings({"unused"})
|
||||
public final class NavigationHelper {
|
||||
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
|
||||
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
|
||||
@@ -69,16 +69,18 @@ public final class NavigationHelper {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull
|
||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
@Nullable final String quality,
|
||||
final boolean resumePlayback) {
|
||||
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue,
|
||||
@Nullable final String quality,
|
||||
final boolean resumePlayback) {
|
||||
final Intent intent = new Intent(context, targetClazz);
|
||||
|
||||
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
|
||||
if (cacheKey != null) {
|
||||
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
|
||||
if (playQueue != null) {
|
||||
final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class);
|
||||
if (cacheKey != null) {
|
||||
intent.putExtra(VideoPlayer.PLAY_QUEUE_KEY, cacheKey);
|
||||
}
|
||||
}
|
||||
if (quality != null) {
|
||||
intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
|
||||
@@ -90,53 +92,51 @@ public final class NavigationHelper {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final boolean resumePlayback) {
|
||||
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue,
|
||||
final boolean resumePlayback) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, null, resumePlayback);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final boolean selectOnAppend,
|
||||
final boolean resumePlayback) {
|
||||
public static <T> Intent getPlayerEnqueueIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue,
|
||||
final boolean selectOnAppend,
|
||||
final boolean resumePlayback) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, resumePlayback)
|
||||
.putExtra(BasePlayer.APPEND_ONLY, true)
|
||||
.putExtra(BasePlayer.SELECT_ON_APPEND, selectOnAppend);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class targetClazz,
|
||||
@NonNull final PlayQueue playQueue,
|
||||
final int repeatMode,
|
||||
final float playbackSpeed,
|
||||
final float playbackPitch,
|
||||
final boolean playbackSkipSilence,
|
||||
@Nullable final String playbackQuality,
|
||||
final boolean resumePlayback,
|
||||
final boolean startPaused,
|
||||
final boolean isMuted) {
|
||||
public static <T> Intent getPlayerIntent(@NonNull final Context context,
|
||||
@NonNull final Class<T> targetClazz,
|
||||
@Nullable final PlayQueue playQueue,
|
||||
final int repeatMode,
|
||||
final float playbackSpeed,
|
||||
final float playbackPitch,
|
||||
final boolean playbackSkipSilence,
|
||||
@Nullable final String playbackQuality,
|
||||
final boolean resumePlayback,
|
||||
final boolean startPaused,
|
||||
final boolean isMuted) {
|
||||
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
|
||||
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
|
||||
.putExtra(BasePlayer.START_PAUSED, startPaused)
|
||||
.putExtra(BasePlayer.IS_MUTED, isMuted);
|
||||
}
|
||||
|
||||
public static void playOnMainPlayer(
|
||||
final AppCompatActivity activity,
|
||||
final PlayQueue queue,
|
||||
final boolean autoPlay) {
|
||||
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) {
|
||||
public static void playOnMainPlayer(final FragmentManager fragmentManager,
|
||||
final PlayQueue queue,
|
||||
final boolean autoPlay) {
|
||||
final PlayQueueItem currentStream = queue.getItem();
|
||||
openVideoDetailFragment(
|
||||
fragmentManager,
|
||||
@@ -148,7 +148,7 @@ public final class NavigationHelper {
|
||||
}
|
||||
|
||||
public static void playOnMainPlayer(@NonNull final Context context,
|
||||
@NonNull final PlayQueue queue,
|
||||
@Nullable final PlayQueue queue,
|
||||
@NonNull final StreamingService.LinkType linkType,
|
||||
@NonNull final String url,
|
||||
@NonNull final String title,
|
||||
@@ -553,18 +553,14 @@ public final class NavigationHelper {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Intent getBackgroundPlayerActivityIntent(final Context context) {
|
||||
return getServicePlayerActivityIntent(context, BackgroundPlayerActivity.class);
|
||||
}
|
||||
|
||||
private static Intent getServicePlayerActivityIntent(final Context context,
|
||||
final Class activityClass) {
|
||||
final Intent intent = new Intent(context, activityClass);
|
||||
public static Intent getPlayQueueActivityIntent(final Context context) {
|
||||
final Intent intent = new Intent(context, BackgroundPlayerActivity.class);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Link handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import androidx.preference.PreferenceManager;
|
||||
@@ -26,7 +27,10 @@ import android.util.TypedValue;
|
||||
import android.view.ContextThemeWrapper;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -231,4 +235,20 @@ public final class ThemeHelper {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(themeKey, defaultTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title to the activity, if the activity is an {@link AppCompatActivity} and has an
|
||||
* action bar.
|
||||
* @param activity the activity to set the title of
|
||||
* @param title the title to set to the activity
|
||||
*/
|
||||
public static void setTitleToAppCompatActivity(@Nullable final Activity activity,
|
||||
final CharSequence title) {
|
||||
if (activity instanceof AppCompatActivity) {
|
||||
final ActionBar actionBar = ((AppCompatActivity) activity).getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import android.util.AttributeSet;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
|
||||
public class CustomCollapsingToolbarLayout extends CollapsingToolbarLayout {
|
||||
@@ -27,8 +28,10 @@ public class CustomCollapsingToolbarLayout extends CollapsingToolbarLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* CollapsingToolbarLayout overrides our logic with fitsSystemWindows and ruins the layout.
|
||||
* Override Google's method
|
||||
* CollapsingToolbarLayout sets it's own setOnApplyInsetsListener which consumes
|
||||
* system insets {@link CollapsingToolbarLayout#onWindowInsetChanged(WindowInsetsCompat)}
|
||||
* so we will not receive them in subviews with fitsSystemWindows = true.
|
||||
* Override Google's behavior
|
||||
* */
|
||||
public void overrideListener() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this, (v, insets) -> insets);
|
||||
|
@@ -1,14 +1,17 @@
|
||||
package org.schabi.newpipe.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
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_FIT;
|
||||
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 resizeMode = RESIZE_MODE_FIT;
|
||||
private int baseHeight = 0;
|
||||
private int maxHeight = 0;
|
||||
private float videoAspectRatio = 0.0f;
|
||||
@@ -30,7 +33,7 @@ public class ExpandableSurfaceView extends SurfaceView {
|
||||
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
|
||||
&& resizeMode != RESIZE_MODE_FIT
|
||||
&& verticalVideo ? maxHeight : baseHeight;
|
||||
|
||||
if (height == 0) {
|
||||
@@ -42,26 +45,22 @@ public class ExpandableSurfaceView extends SurfaceView {
|
||||
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;
|
||||
if (resizeMode == RESIZE_MODE_FIT
|
||||
// KitKat doesn't work well when a view has a scale like needed for ZOOM
|
||||
|| (resizeMode == RESIZE_MODE_ZOOM && VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)) {
|
||||
if (aspectDeformation > 0) {
|
||||
height = (int) (width / videoAspectRatio);
|
||||
} else {
|
||||
width = (int) (height * videoAspectRatio);
|
||||
}
|
||||
} else if (resizeMode == RESIZE_MODE_ZOOM) {
|
||||
if (aspectDeformation < 0) {
|
||||
scaleY = viewAspectRatio / videoAspectRatio;
|
||||
} else {
|
||||
scaleX = videoAspectRatio / viewAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
Reference in New Issue
Block a user