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

This commit is contained in:
wb9688 2017-11-11 13:18:26 +01:00
commit 621a1909ec
245 changed files with 11209 additions and 2124 deletions

View file

@ -12,7 +12,6 @@ import java.io.InterruptedIOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
@ -135,11 +134,8 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
}
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
for (Map.Entry<String, List<String>> entry : con.getHeaderFields().entrySet()) {
System.err.println(entry.getKey() + ": " + entry.getValue());
}
String inputLine;
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}

View file

@ -27,6 +27,7 @@ import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
@ -119,6 +120,12 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
});
}
if(sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
if (DEBUG) Log.d(TAG, "main page has changed, recreating main fragment...");
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
NavigationHelper.openMainActivity(this);
}
}
@Override
@ -175,7 +182,6 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayHomeAsUpEnabled(false);
}
return true;
@ -304,7 +310,7 @@ public class MainActivity extends AppCompatActivity implements HistoryListener {
}
@Override
public void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream) {
public void onVideoPlayed(StreamInfo streamInfo, @Nullable VideoStream videoStream) {
addWatchHistoryEntry(streamInfo);
}

View file

@ -10,22 +10,22 @@ import org.schabi.newpipe.util.NavigationHelper;
import java.util.Collection;
import java.util.HashSet;
/*
/**
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* RouterActivity .java is part of NewPipe.
* RouterActivity.java is part of NewPipe.
*
* OpenHitboxStreams is free software: you can redistribute it and/or modify
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenHitboxStreams is distributed in the hope that it will be useful,
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenHitboxStreams. If not, see <http://www.gnu.org/licenses/>.
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
@ -43,9 +43,8 @@ public class RouterActivity extends AppCompatActivity {
}
protected void handleUrl(String url) {
try {
NavigationHelper.openByLink(this, url);
} catch (Exception e) {
boolean success = NavigationHelper.openByLink(this, url);
if (!success) {
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
}

View file

@ -50,6 +50,10 @@ public class License implements Parcelable {
public String getAbbreviation() {
return abbreviation;
}
public String getFilename() {
return filename;
}
@Override
public int describeContents() {

View file

@ -1,22 +1,13 @@
package org.schabi.newpipe.about;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.view.*;
import android.widget.TextView;
import org.schabi.newpipe.R;
import java.util.Arrays;
@ -48,25 +39,7 @@ public class LicenseFragment extends Fragment {
* @param license the license to show
*/
public static void showLicense(Context context, License license) {
if(context == null) {
throw new NullPointerException("context is null");
}
if(license == null) {
throw new NullPointerException("license is null");
}
AlertDialog.Builder alert = new AlertDialog.Builder(context);
alert.setTitle(license.getName());
WebView wv = new WebView(context);
wv.loadUrl(license.getContentUri().toString());
alert.setView(wv);
alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
alert.show();
new LicenseFragmentHelper().execute(context, license);
}
@Override
@ -111,7 +84,6 @@ public class LicenseFragment extends Fragment {
});
softwareComponentsView.addView(componentView);
registerForContextMenu(componentView);
}
return rootView;
}

View file

@ -0,0 +1,111 @@
package org.schabi.newpipe.about;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.support.v7.app.AlertDialog;
import android.webkit.WebView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
private Context context;
private License license;
@Override
protected Integer doInBackground(Object... objects) {
context = (Context) objects[0];
license = (License) objects[1];
return 1;
}
@Override
protected void onPostExecute(Integer result){
String webViewData = getFormattedLicense(context, license);
AlertDialog.Builder alert = new AlertDialog.Builder(context);
alert.setTitle(license.getName());
WebView wv = new WebView(context);
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
alert.setView(wv);
alert.setNegativeButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
alert.show();
}
/**
* @param context the context to use
* @param license the license
* @return String which contains a HTML formatted license page styled according to the context's theme
*/
public static String getFormattedLicense(Context context, License license) {
if(context == null) {
throw new NullPointerException("context is null");
}
if(license == null) {
throw new NullPointerException("license is null");
}
String licenseContent = "";
String webViewData;
try {
BufferedReader in = new BufferedReader(new InputStreamReader(context.getAssets().open(license.getFilename()), "UTF-8"));
String str;
while ((str = in.readLine()) != null) {
licenseContent += str;
}
in.close();
// split the HTML file and insert the stylesheet into the HEAD of the file
String[] insert = licenseContent.split("</head>");
webViewData = insert[0] + "<style type=\"text/css\">"
+ getLicenseStylesheet(context) + "</style></head>"
+ insert[1];
} catch (Exception e) {
throw new NullPointerException("could not get license file:" + getLicenseStylesheet(context));
}
return webViewData;
}
/**
*
* @param context
* @return String which is a CSS stylesheet according to the context's theme
*/
public static String getLicenseStylesheet(Context context) {
boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
return "body{padding:12px 15px;margin:0;background:#"
+ getHexRGBColor(context, isLightTheme
? R.color.light_license_background_color
: R.color.dark_license_background_color)
+ ";color:#"
+ getHexRGBColor(context, isLightTheme
? R.color.light_license_text_color
: R.color.dark_license_text_color) + ";}"
+ "a[href]{color:#"
+ getHexRGBColor(context, isLightTheme
? R.color.light_youtube_primary_color
: R.color.dark_youtube_primary_color) + ";}"
+ "pre{white-space: pre-wrap;}";
}
/**
* Cast R.color to a hexadecimal color value
* @param context the context to use
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
public static String getHexRGBColor(Context context, int color) {
return context.getResources().getString(color).substring(3);
}
}

View file

@ -11,6 +11,7 @@ import io.reactivex.Flowable;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
@ -27,11 +28,20 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
@Override
int deleteAll();
@Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query")
int deleteAllWhereQuery(String query);
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<SearchHistoryEntry>> getAll();
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit")
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit")
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
}

View file

@ -7,6 +7,7 @@ import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.util.Constants;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
@ -28,7 +29,7 @@ public class SubscriptionEntity {
private long uid = 0;
@ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
private int serviceId = -1;
private int serviceId = Constants.NO_SERVICE_ID;
@ColumnInfo(name = SUBSCRIPTION_URL)
private String url;

View file

@ -50,6 +50,8 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
protected Button errorButtonRetry;
protected TextView errorTextView;
protected boolean useAsFrontPage = false;
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
@ -62,6 +64,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
wasLoading.set(isLoading.get());
}
public void useAsFrontPage(boolean value) {
useAsFrontPage = value;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/

View file

@ -2,6 +2,7 @@ package org.schabi.newpipe.fragments;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -13,6 +14,24 @@ public class BlankFragment extends BaseFragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
if(activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar()
.setTitle("NewPipe");
}
return inflater.inflate(R.layout.fragment_blank, container, false);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser) {
if(activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar()
.setTitle("NewPipe");
}
// leave this inline. Will make it harder for copy cats.
// If you are a Copy cat FUCK YOU.
// I WILL FIND YOU, AND I WILL ...
}
}
}

View file

@ -1,7 +1,7 @@
package org.schabi.newpipe.fragments;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
@ -9,21 +9,51 @@ import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.concurrent.ExecutionException;
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
private ViewPager viewPager;
private boolean showBlankTab = false;
private static final int FALLBACK_SERVICE_ID = 0; // Youtbe
private static final String FALLBACK_CHANNEL_URL =
"https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ";
private static final String FALLBACK_CHANNEL_NAME = "Music";
private static final String FALLBACK_KIOSK_ID = "Trending";
public int currentServiceId = -1;
/*//////////////////////////////////////////////////////////////////////////
// Konst
//////////////////////////////////////////////////////////////////////////*/
private static final int KIOSK_MENU_OFFSETT = 2000;
/*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle
@ -37,6 +67,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
currentServiceId = Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(getActivity())
.getString(getString(R.string.current_service_key), "0"));
return inflater.inflate(R.layout.fragment_main, container, false);
}
@ -53,6 +85,28 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
viewPager.setOffscreenPageLimit(adapter.getCount());
tabLayout.setupWithViewPager(viewPager);
if(ThemeHelper.isLightThemeSelected(getActivity())) {
tabLayout.setBackgroundColor(getResources().getColor(R.color.light_youtube_primary_color));
}
if(PreferenceManager.getDefaultSharedPreferences(getActivity())
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
.equals(getString(R.string.subscription_page_key))) {
if(ThemeHelper.isLightThemeSelected(getActivity())) {
tabLayout.getTabAt(0).setIcon(R.drawable.ic_channel_black_24dp);
} else{
tabLayout.getTabAt(0).setIcon(R.drawable.ic_channel_white_24dp);
}
} else {
if(ThemeHelper.isLightThemeSelected(getActivity())) {
tabLayout.getTabAt(0).setIcon(R.drawable.ic_whatshot_black_24dp);
tabLayout.getTabAt(1).setIcon(R.drawable.ic_channel_black_24dp);
} else {
tabLayout.getTabAt(0).setIcon(R.drawable.ic_whatshot_white_24dp);
tabLayout.getTabAt(1).setIcon(R.drawable.ic_channel_white_24dp);
}
}
}
/*//////////////////////////////////////////////////////////////////////////
@ -64,10 +118,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
inflater.inflate(R.menu.main_fragment_menu, menu);
SubMenu kioskMenu = menu.addSubMenu(getString(R.string.kiosk));
try {
createKioskMenu(kioskMenu, inflater);
} catch (Exception e) {
ErrorActivity.reportError(activity, e,
activity.getClass(),
null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
}
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(false);
supportActionBar.setDisplayHomeAsUpEnabled(false);
}
}
@ -113,6 +176,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
if(PreferenceManager.getDefaultSharedPreferences(getActivity())
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
.equals(getString(R.string.subscription_page_key))) {
return new SubscriptionFragment();
} else {
return getMainPageFramgent();
}
case 1:
return new SubscriptionFragment();
default:
@ -122,12 +193,99 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
@Override
public CharSequence getPageTitle(int position) {
return getString(this.tabTitles[position]);
//return getString(this.tabTitles[position]);
return "";
}
@Override
public int getCount() {
return this.tabTitles.length;
if(PreferenceManager.getDefaultSharedPreferences(getActivity())
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
.equals(getString(R.string.subscription_page_key))) {
return 1;
} else {
return 2;
}
}
}
/*//////////////////////////////////////////////////////////////////////////
// Main page content
//////////////////////////////////////////////////////////////////////////*/
private Fragment getMainPageFramgent() {
try {
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(getActivity());
final String setMainPage = preferences.getString(getString(R.string.main_page_content_key),
getString(R.string.main_page_selectd_kiosk_id));
if(setMainPage.equals(getString(R.string.blank_page_key))) {
return new BlankFragment();
} else if(setMainPage.equals(getString(R.string.kiosk_page_key))) {
int serviceId = preferences.getInt(getString(R.string.main_page_selected_service),
FALLBACK_SERVICE_ID);
String kioskId = preferences.getString(getString(R.string.main_page_selectd_kiosk_id),
FALLBACK_KIOSK_ID);
KioskFragment fragment = KioskFragment.getInstance(serviceId, kioskId
);
fragment.useAsFrontPage(true);
return fragment;
} else if(setMainPage.equals(getString(R.string.feed_page_key))) {
FeedFragment fragment = new FeedFragment();
fragment.useAsFrontPage(true);
return fragment;
} else if(setMainPage.equals(getString(R.string.channel_page_key))) {
int serviceId = preferences.getInt(getString(R.string.main_page_selected_service),
FALLBACK_SERVICE_ID);
String url = preferences.getString(getString(R.string.main_page_selected_channel_url),
FALLBACK_CHANNEL_URL);
String name = preferences.getString(getString(R.string.main_page_selected_channel_name),
FALLBACK_CHANNEL_NAME);
ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name);
fragment.useAsFrontPage(true);
return fragment;
} else {
return new BlankFragment();
}
} catch (Exception e) {
ErrorActivity.reportError(activity, e,
activity.getClass(),
null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
return new BlankFragment();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Select Kiosk
//////////////////////////////////////////////////////////////////////////*/
private void createKioskMenu(Menu menu, MenuInflater menuInflater)
throws Exception {
StreamingService service = NewPipe.getService(currentServiceId);
KioskList kl = service.getKioskList();
int i = 0;
for(final String ks : kl.getAvailableKiosks()) {
menu.add(0, KIOSK_MENU_OFFSETT + i, Menu.NONE,
KioskTranslator.getTranslatedKioskName(ks, getContext()))
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
try {
NavigationHelper.openKioskFragment(getFragmentManager(), currentServiceId, ks);
} catch (Exception e) {
ErrorActivity.reportError(activity, e,
activity.getClass(),
null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
}
return true;
}
});
i++;
}
}
}

View file

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

View file

@ -135,7 +135,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void selected(StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
NavigationHelper.openVideoDetailFragment(
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
selectedItem.service_id, selectedItem.url, selectedItem.name);
}
});
@ -143,7 +145,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void selected(ChannelInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
NavigationHelper.openChannelFragment(
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
selectedItem.service_id, selectedItem.url, selectedItem.name);
}
});
@ -151,7 +155,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void selected(PlaylistInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
NavigationHelper.openPlaylistFragment(
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
selectedItem.service_id, selectedItem.url, selectedItem.name);
}
});
@ -181,7 +187,11 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
supportActionBar.setDisplayHomeAsUpEnabled(true);
if(useAsFrontPage) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
}
}

View file

@ -8,6 +8,7 @@ import android.view.View;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.util.Constants;
import java.util.Queue;
@ -21,7 +22,7 @@ import io.reactivex.schedulers.Schedulers;
public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> {
@State
protected int serviceId = -1;
protected int serviceId = Constants.NO_SERVICE_ID;
@State
protected String name;
@State

View file

@ -7,6 +7,7 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@ -80,6 +81,20 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(activity != null
&& useAsFrontPage
&& isVisibleToUser) {
try {
activity.getSupportActionBar().setTitle(currentInfo.name);
} catch (Exception e) {
onError(e);
}
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@ -109,6 +124,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
return headerRootLayout;
}
@ -118,30 +134,59 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_channel, menu);
ActionBar supportActionBar = activity.getSupportActionBar();
if(useAsFrontPage) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
menuRssButton = menu.findItem(R.id.menu_item_rss);
if (currentInfo != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.feed_url));
}
menuRssButton = menu.findItem(R.id.menu_item_rss);
if (currentInfo != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.feed_url));
}
}
private void openRssFeed() {
final ChannelInfo info = currentInfo;
if(info != null) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.feed_url));
startActivity(intent);
}
}
private void openChannelUriInBrowser() {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
}
private void shareChannelUri() {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, url);
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_rss: {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(currentInfo.feed_url));
startActivity(intent);
return true;
case R.id.menu_item_rss:
openRssFeed();
break;
case R.id.menu_item_openInBrowser:
openChannelUriInBrowser();
break;
case R.id.menu_item_share: {
shareChannelUri();
break;
}
default:
return super.onOptionsItemSelected(item);
}
return true;
}
/*//////////////////////////////////////////////////////////////////////////

View file

@ -36,10 +36,8 @@ import io.reactivex.MaybeObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers;
public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Void> {
@ -121,6 +119,11 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
if (supportActionBar != null) {
supportActionBar.setTitle(R.string.fragment_whats_new);
}
if(useAsFrontPage) {
supportActionBar.setDisplayShowTitleEnabled(true);
//supportActionBar.setDisplayShowTitleEnabled(false);
}
}
@Override

View file

@ -0,0 +1,173 @@
package org.schabi.newpipe.fragments.list.kiosk;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.NavigationHelper;
import io.reactivex.Single;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
/**
* Created by Christian Schabesberger on 23.09.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* KioskFragment.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
private String kioskId = "";
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private TextView headerTitleView;
public static KioskFragment getInstance(int serviceId)
throws ExtractionException {
return getInstance(serviceId, NewPipe.getService(serviceId)
.getKioskList()
.getDefaultKioskId());
}
public static KioskFragment getInstance(int serviceId, String kioskId)
throws ExtractionException {
KioskFragment instance = new KioskFragment();
StreamingService service = NewPipe.getService(serviceId);
UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList()
.getUrlIdHandlerByType(kioskId);
instance.setInitialData(serviceId,
kioskTypeUrlIdHandler.getUrl(kioskId),
kioskId);
instance.kioskId = kioskId;
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(useAsFrontPage && isVisibleToUser) {
try {
activity.getSupportActionBar().setTitle(KioskTranslator.getTranslatedKioskName(kioskId, getActivity()));
} catch (Exception e) {
onError(e);
}
}
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_kiosk, container, false);
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null && useAsFrontPage) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
public Single<KioskInfo> loadResult(boolean forceReload) {
String contentCountry = PreferenceManager
.getDefaultSharedPreferences(activity)
.getString(getString(R.string.search_language_key),
getString(R.string.default_language_value));
return ExtractorHelper.getKioskInfo(serviceId, url, contentCountry, forceReload);
}
@Override
public Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextItemsUrl);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateView(itemsList, false, 100);
}
@Override
public void handleResult(@NonNull final KioskInfo result) {
super.handleResult(result);
String title = KioskTranslator.getTranslatedKioskName(result.id, getActivity());
ActionBar supportActionBar = activity.getSupportActionBar();
supportActionBar.setTitle(title);
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors,
UserAction.REQUESTED_PLAYLIST,
NewPipe.getNameOfService(result.service_id), result.url, 0);
}
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
super.handleNextItems(result);
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors,
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
, "Get next page of: " + url, 0);
}
}
}

View file

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

View file

@ -2,14 +2,16 @@ package org.schabi.newpipe.fragments.list.search;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.TooltipCompat;
import android.text.Editable;
import android.text.TextUtils;
@ -25,35 +27,50 @@ import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.TextView;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SearchResult;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.history.HistoryListener;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import icepick.State;
import io.reactivex.Flowable;
import io.reactivex.Notification;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.BiFunction;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.functions.Predicate;
@ -62,69 +79,79 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> {
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> implements BackPressable {
/*//////////////////////////////////////////////////////////////////////////
// Search
//////////////////////////////////////////////////////////////////////////*/
/**
* The suggestions will appear only if the query meet this threshold (>=).
* The suggestions will only be fetched from network if the query meet this threshold (>=).
* (local ones will be fetched regardless of the length)
*/
private static final int THRESHOLD_SUGGESTION = 3;
private static final int THRESHOLD_NETWORK_SUGGESTION = 1;
/**
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
*/
private static final int SUGGESTIONS_DEBOUNCE = 150; //ms
private static final int SUGGESTIONS_DEBOUNCE = 120; //ms
@State
protected int filterItemCheckedId = -1;
private SearchEngine.Filter filter = SearchEngine.Filter.ANY;
@State
protected int serviceId = -1;
protected int serviceId = Constants.NO_SERVICE_ID;
@State
protected String searchQuery = "";
protected String searchQuery;
@State
protected String lastSearchedQuery;
@State
protected boolean wasSearchFocused = false;
private int currentPage = 0;
private int currentNextPage = 0;
private String searchLanguage;
private boolean showSuggestions = true;
private boolean isSuggestionsEnabled = true;
private boolean isSearchHistoryEnabled = true;
private PublishSubject<String> suggestionPublisher = PublishSubject.create();
private Disposable searchDisposable;
private Disposable suggestionWorkerDisposable;
private Disposable suggestionDisposable;
private CompositeDisposable disposables = new CompositeDisposable();
private SuggestionListAdapter suggestionListAdapter;
private SearchHistoryDAO searchHistoryDAO;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View searchToolbarContainer;
private AutoCompleteTextView searchEditText;
private EditText searchEditText;
private View searchClear;
private View suggestionsPanel;
private RecyclerView suggestionsRecyclerView;
/*////////////////////////////////////////////////////////////////////////*/
public static SearchFragment getInstance(int serviceId, String query) {
SearchFragment searchFragment = new SearchFragment();
searchFragment.setQuery(serviceId, query);
searchFragment.searchOnResume();
if (!TextUtils.isEmpty(query)) {
searchFragment.setSearchOnResume();
}
return searchFragment;
}
/**
* Set wasLoading to true so when the fragment onResume is called, the initial search is done.
* (it will only start searching if the query is not null or empty)
*/
private void searchOnResume() {
if (!TextUtils.isEmpty(searchQuery)) {
wasLoading.set(true);
}
private void setSearchOnResume() {
wasLoading.set(true);
}
/*//////////////////////////////////////////////////////////////////////////
@ -134,7 +161,22 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
public void onAttach(Context context) {
super.onAttach(context);
suggestionListAdapter = new SuggestionListAdapter(activity);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
isSearchHistoryEnabled = preferences.getBoolean(getString(R.string.enable_search_history_key), true);
suggestionListAdapter.setShowSugestinHistory(isSearchHistoryEnabled);
searchHistoryDAO = NewPipeDatabase.getInstance().searchHistoryDAO();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
isSuggestionsEnabled = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value));
}
@Override
@ -142,14 +184,23 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
return inflater.inflate(R.layout.fragment_search, container, false);
}
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
showSearchOnStart();
initSearchListeners();
}
@Override
public void onPause() {
super.onPause();
wasSearchFocused = searchEditText.hasFocus();
if (searchDisposable != null) searchDisposable.dispose();
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
hideSoftKeyboard(searchEditText);
if (suggestionDisposable != null) suggestionDisposable.dispose();
if (disposables != null) disposables.clear();
hideKeyboardSearch();
}
@Override
@ -157,10 +208,6 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
if (DEBUG) Log.d(TAG, "onResume() called");
super.onResume();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
showSuggestions = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value));
if (!TextUtils.isEmpty(searchQuery)) {
if (wasLoading.getAndSet(false)) {
if (currentNextPage > currentPage) loadMoreItems();
@ -175,7 +222,16 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
}
}
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) {
showKeyboardSearch();
showSuggestionsPanel();
} else {
hideKeyboardSearch();
hideSuggestionsPanel();
}
wasSearchFocused = false;
}
@Override
@ -188,17 +244,16 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
public void onDestroy() {
super.onDestroy();
if (!activity.isChangingConfigurations()) StateSaver.onDestroy(savedState);
if (searchDisposable != null) searchDisposable.dispose();
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
if (suggestionDisposable != null) suggestionDisposable.dispose();
if (disposables != null) disposables.clear();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) {
search(searchQuery);
} else Log.e(TAG, "ReCaptcha failed");
break;
@ -209,6 +264,23 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
}
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
suggestionsPanel = rootView.findViewById(R.id.suggestions_panel);
suggestionsRecyclerView = rootView.findViewById(R.id.suggestions_list);
suggestionsRecyclerView.setAdapter(suggestionListAdapter);
suggestionsRecyclerView.setLayoutManager(new LayoutManagerSmoothScroller(activity));
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@ -229,8 +301,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
public void onSaveInstanceState(Bundle bundle) {
searchQuery = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
? searchEditText.getText().toString() : searchQuery;
searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery;
super.onSaveInstanceState(bundle);
}
@ -245,7 +316,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
} else {
if (searchEditText != null) {
searchEditText.setText("");
showSoftKeyboard(searchEditText);
showKeyboardSearch();
}
animateView(errorPanelRoot, false, 200);
}
@ -266,12 +337,6 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
}
inflater.inflate(R.menu.menu_search, menu);
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
setupSearchView();
restoreFilterChecked(menu, filterItemCheckedId);
}
@ -301,14 +366,13 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
private SearchEngine.Filter getFilterFromMenuId(int itemId) {
switch (itemId) {
case R.id.menu_filter_all:
return SearchEngine.Filter.ANY;
case R.id.menu_filter_video:
return SearchEngine.Filter.STREAM;
case R.id.menu_filter_channel:
return SearchEngine.Filter.CHANNEL;
case R.id.menu_filter_playlist:
return SearchEngine.Filter.PLAYLIST;
case R.id.menu_filter_all:
default:
return SearchEngine.Filter.ANY;
}
@ -320,9 +384,9 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
private TextWatcher textWatcher;
private void setupSearchView() {
searchEditText.setText(searchQuery != null ? searchQuery : "");
searchEditText.setAdapter(suggestionListAdapter);
private void showSearchOnStart() {
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery);
searchEditText.setText(searchQuery);
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
searchToolbarContainer.setTranslationX(100);
@ -334,15 +398,10 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
searchToolbarContainer.setAlpha(1f);
searchToolbarContainer.setVisibility(View.VISIBLE);
}
initSearchListeners();
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) showSoftKeyboard(searchEditText);
else hideSoftKeyboard(searchEditText);
wasSearchFocused = false;
}
private void initSearchListeners() {
if (DEBUG) Log.d(TAG, "initSearchListeners() called");
searchClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -352,11 +411,9 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
searchEditText.setText("", false);
} else searchEditText.setText("");
suggestionListAdapter.updateAdapter(new ArrayList<String>());
showSoftKeyboard(searchEditText);
searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<SuggestionItem>());
showKeyboardSearch();
}
});
@ -366,7 +423,9 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
public void onClick(View v) {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
searchEditText.showDropDown();
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
}
});
@ -374,22 +433,24 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
if (hasFocus) searchEditText.showDropDown();
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
showSuggestionsPanel();
}
}
});
searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (DEBUG) {
Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
}
String s = suggestionListAdapter.getSuggestion(position);
if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
submitQuery(s);
public void onSuggestionItemSelected(SuggestionItem item) {
search(item.query);
searchEditText.setText(item.query);
}
@Override
public void onSuggestionItemLongClick(SuggestionItem item) {
if (item.fromHistory) showDeleteSuggestionDialog(item);
}
});
searchEditText.setThreshold(THRESHOLD_SUGGESTION);
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
textWatcher = new TextWatcher() {
@ -404,32 +465,32 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
public void afterTextChanged(Editable s) {
String newText = searchEditText.getText().toString();
if (!TextUtils.isEmpty(newText)) suggestionPublisher.onNext(newText);
suggestionPublisher.onNext(newText);
}
};
searchEditText.addTextChangedListener(textWatcher);
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (DEBUG)
if (DEBUG) {
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
}
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
submitQuery(searchEditText.getText().toString());
search(searchEditText.getText().toString());
return true;
}
return false;
}
});
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
}
private void unsetSearchListeners() {
if (DEBUG) Log.d(TAG, "unsetSearchListeners() called");
searchClear.setOnClickListener(null);
searchClear.setOnLongClickListener(null);
searchEditText.setOnClickListener(null);
searchEditText.setOnItemClickListener(null);
searchEditText.setOnFocusChangeListener(null);
searchEditText.setOnEditorActionListener(null);
@ -437,68 +498,166 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
textWatcher = null;
}
private void showSoftKeyboard(View view) {
if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
if (view == null) return;
private void showSuggestionsPanel() {
if (DEBUG) Log.d(TAG, "showSuggestionsPanel() called");
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, true, 200);
}
if (view.requestFocus()) {
private void hideSuggestionsPanel() {
if (DEBUG) Log.d(TAG, "hideSuggestionsPanel() called");
animateView(suggestionsPanel, AnimationUtils.Type.LIGHT_SLIDE_AND_ALPHA, false, 200);
}
private void showKeyboardSearch() {
if (DEBUG) Log.d(TAG, "showKeyboardSearch() called");
if (searchEditText == null) return;
if (searchEditText.requestFocus()) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
}
}
private void hideSoftKeyboard(View view) {
if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
if (view == null) return;
private void hideKeyboardSearch() {
if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called");
if (searchEditText == null) return;
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
view.clearFocus();
searchEditText.clearFocus();
}
private void showDeleteSuggestionDialog(final SuggestionItem item) {
new AlertDialog.Builder(activity)
.setTitle(item.query)
.setMessage(R.string.delete_item_search_history)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
disposables.add(Observable
.fromCallable(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return searchHistoryDAO.deleteAllWhereQuery(item.query);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer howManyDeleted) throws Exception {
suggestionPublisher.onNext(searchEditText.getText().toString());
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
showSnackBarError(throwable, UserAction.SOMETHING_ELSE, "none", "Deleting item failed", R.string.general_error);
}
}));
}
}).show();
}
@Override
public boolean onBackPressed() {
if (suggestionsPanel.getVisibility() == View.VISIBLE && infoListAdapter.getItemsList().size() > 0 && !isLoading.get()) {
hideSuggestionsPanel();
hideKeyboardSearch();
searchEditText.setText(lastSearchedQuery);
return true;
}
return false;
}
public void giveSearchEditTextFocus() {
showSoftKeyboard(searchEditText);
showKeyboardSearch();
}
private void initSuggestionObserver() {
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
final Predicate<String> checkEnabledAndLength = new Predicate<String>() {
@Override
public boolean test(@io.reactivex.annotations.NonNull String s) throws Exception {
boolean lengthCheck = s.length() >= THRESHOLD_SUGGESTION;
// Clear the suggestions adapter if the length check fails
if (!lengthCheck && !suggestionListAdapter.isEmpty()) {
suggestionListAdapter.updateAdapter(new ArrayList<String>());
}
// Only pass through if suggestions is enabled and the query length is equal or greater than THRESHOLD_SUGGESTION
return showSuggestions && lengthCheck;
}
};
if (DEBUG) Log.d(TAG, "initSuggestionObserver() called");
if (suggestionDisposable != null) suggestionDisposable.dispose();
suggestionWorkerDisposable = suggestionPublisher
final Observable<String> observable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWith(!TextUtils.isEmpty(searchQuery) ? searchQuery : "")
.filter(checkEnabledAndLength)
.switchMap(new Function<String, Observable<Notification<List<String>>>>() {
.startWith(searchQuery != null ? searchQuery : "")
.filter(new Predicate<String>() {
@Override
public Observable<Notification<List<String>>> apply(@io.reactivex.annotations.NonNull String query) throws Exception {
return ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable().materialize();
public boolean test(@io.reactivex.annotations.NonNull String query) throws Exception {
return isSuggestionsEnabled;
}
});
suggestionDisposable = observable
.switchMap(new Function<String, ObservableSource<Notification<List<SuggestionItem>>>>() {
@Override
public ObservableSource<Notification<List<SuggestionItem>>> apply(@io.reactivex.annotations.NonNull final String query) throws Exception {
final Flowable<List<SearchHistoryEntry>> flowable = query.length() > 0
? searchHistoryDAO.getSimilarEntries(query, 3)
: searchHistoryDAO.getUniqueEntries(25);
final Observable<List<SuggestionItem>> local = flowable.toObservable()
.map(new Function<List<SearchHistoryEntry>, List<SuggestionItem>>() {
@Override
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SearchHistoryEntry> searchHistoryEntries) throws Exception {
List<SuggestionItem> result = new ArrayList<>();
for (SearchHistoryEntry entry : searchHistoryEntries)
result.add(new SuggestionItem(true, entry.getSearch()));
return result;
}
});
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
// Only pass through if the query length is equal or greater than THRESHOLD_NETWORK_SUGGESTION
return local.materialize();
}
final Observable<List<SuggestionItem>> network = ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable()
.map(new Function<List<String>, List<SuggestionItem>>() {
@Override
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<String> strings) throws Exception {
List<SuggestionItem> result = new ArrayList<>();
for (String entry : strings) result.add(new SuggestionItem(false, entry));
return result;
}
});
return Observable.zip(local, network, new BiFunction<List<SuggestionItem>, List<SuggestionItem>, List<SuggestionItem>>() {
@Override
public List<SuggestionItem> apply(@io.reactivex.annotations.NonNull List<SuggestionItem> localResult, @io.reactivex.annotations.NonNull List<SuggestionItem> networkResult) throws Exception {
List<SuggestionItem> result = new ArrayList<>();
if (localResult.size() > 0) result.addAll(localResult);
// Remove duplicates
final Iterator<SuggestionItem> iterator = networkResult.iterator();
while (iterator.hasNext() && localResult.size() > 0) {
final SuggestionItem next = iterator.next();
for (SuggestionItem item : localResult) {
if (item.query.equals(next.query)) {
iterator.remove();
break;
}
}
}
if (networkResult.size() > 0) result.addAll(networkResult);
return result;
}
}).materialize();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Notification<List<String>>>() {
.subscribe(new Consumer<Notification<List<SuggestionItem>>>() {
@Override
public void accept(@io.reactivex.annotations.NonNull Notification<List<String>> listNotification) throws Exception {
public void accept(@io.reactivex.annotations.NonNull Notification<List<SuggestionItem>> listNotification) throws Exception {
if (listNotification.isOnNext()) {
handleSuggestions(listNotification.getValue());
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading();
}
} else if (listNotification.isOnError()) {
Throwable error = listNotification.getError();
if (!ExtractorHelper.isInterruptedCaused(error)) {
if (!ExtractorHelper.hasAssignableCauseThrowable(error,
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class)) {
onSuggestionError(error);
}
}
@ -513,25 +672,58 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
private void search(final String query) {
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");
if (query.isEmpty()) return;
hideSoftKeyboard(searchEditText);
this.searchQuery = query;
this.currentPage = 0;
try {
final StreamingService service = NewPipe.getServiceByUrl(query);
if (service != null) {
showLoading();
disposables.add(Observable
.fromCallable(new Callable<Intent>() {
@Override
public Intent call() throws Exception {
return NavigationHelper.getIntentByLink(activity, service, query);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Intent>() {
@Override
public void accept(Intent intent) throws Exception {
getFragmentManager().popBackStackImmediate();
activity.startActivity(intent);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
showError(getString(R.string.url_not_supported_toast), false);
}
}));
return;
}
} catch (Exception e) {
// Exception occurred, it's not a url
}
lastSearchedQuery = query;
searchQuery = query;
currentPage = 0;
infoListAdapter.clearStreamItemList();
hideSuggestionsPanel();
hideKeyboardSearch();
if (activity instanceof HistoryListener) {
((HistoryListener) activity).onSearch(serviceId, query);
suggestionPublisher.onNext(query);
}
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
final String searchLanguageKey = getContext().getString(R.string.search_language_key);
searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value));
startLoading(false);
}
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
if (disposables != null) disposables.clear();
if (searchDisposable != null) searchDisposable.dispose();
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, searchLanguage, filter)
.subscribeOn(Schedulers.io())
@ -584,7 +776,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
@Override
protected void onItemSelected(InfoItem selectedItem) {
super.onItemSelected(selectedItem);
hideSoftKeyboard(searchEditText);
hideKeyboardSearch();
}
/*//////////////////////////////////////////////////////////////////////////
@ -595,13 +787,10 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
this.filter = filter;
this.filterItemCheckedId = item.getItemId();
item.setChecked(true);
if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery);
}
private void submitQuery(String query) {
if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
if (query.isEmpty()) return;
search(query);
if (!TextUtils.isEmpty(searchQuery)) {
search(searchQuery);
}
}
private void setQuery(int serviceId, String searchQuery) {
@ -609,19 +798,23 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
this.searchQuery = searchQuery;
}
@Override
public void showError(String message, boolean showRetryButton) {
super.showError(message, showRetryButton);
hideSoftKeyboard(searchEditText);
}
/*//////////////////////////////////////////////////////////////////////////
// Suggestion Results
//////////////////////////////////////////////////////////////////////////*/
public void handleSuggestions(@NonNull List<String> suggestions) {
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
suggestionListAdapter.updateAdapter(suggestions);
suggestionsRecyclerView.smoothScrollToPosition(0);
suggestionsRecyclerView.post(new Runnable() {
@Override
public void run() {
suggestionListAdapter.setItems(suggestions);
}
});
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading();
}
}
public void onSuggestionError(Throwable exception) {
@ -642,6 +835,13 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
showListFooter(false);
}
@Override
public void showError(String message, boolean showRetryButton) {
super.showError(message, showRetryButton);
hideSuggestionsPanel();
hideKeyboardSearch();
}
/*//////////////////////////////////////////////////////////////////////////
// Search Results
//////////////////////////////////////////////////////////////////////////*/
@ -652,6 +852,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
}
lastSearchedQuery = searchQuery;
if (infoListAdapter.getItemsList().size() == 0) {
if (result.resultList.size() > 0) {
infoListAdapter.addInfoItemList(result.resultList);

View file

@ -0,0 +1,16 @@
package org.schabi.newpipe.fragments.list.search;
public class SuggestionItem {
public final boolean fromHistory;
public final String query;
public SuggestionItem(boolean fromHistory, String query) {
this.fromHistory = fromHistory;
this.query = query;
}
@Override
public String toString() {
return "[" + fromHistory + "" + query + "]";
}
}

View file

@ -1,89 +1,122 @@
package org.schabi.newpipe.fragments.list.search;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.support.v4.widget.ResourceCursorAdapter;
import android.content.res.TypedArray;
import android.support.annotation.AttrRes;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import java.util.ArrayList;
import java.util.List;
/*
* Created by Christian Schabesberger on 02.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* SuggestionListAdapter.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* {@link ResourceCursorAdapter} to display suggestions.
*/
public class SuggestionListAdapter extends ResourceCursorAdapter {
private static final String[] columns = new String[]{"_id", "title"};
private static final int INDEX_ID = 0;
private static final int INDEX_TITLE = 1;
public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
private final ArrayList<SuggestionItem> items = new ArrayList<>();
private final Context context;
private OnSuggestionItemSelected listener;
private boolean showSugestinHistory = true;
public interface OnSuggestionItemSelected {
void onSuggestionItemSelected(SuggestionItem item);
void onSuggestionItemLongClick(SuggestionItem item);
}
public SuggestionListAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1, null, 0);
this.context = context;
}
public void setItems(List<SuggestionItem> items) {
this.items.clear();
if (showSugestinHistory) {
this.items.addAll(items);
} else {
// remove history items if history is disabled
for (SuggestionItem item : items) {
if (!item.fromHistory) {
this.items.add(item);
}
}
}
notifyDataSetChanged();
}
public void setListener(OnSuggestionItemSelected listener) {
this.listener = listener;
}
public void setShowSugestinHistory(boolean v) {
showSugestinHistory = v;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ViewHolder viewHolder = new ViewHolder(view);
viewHolder.suggestionTitle.setText(cursor.getString(INDEX_TITLE));
}
/**
* Update the suggestion list
* @param suggestions the list of suggestions
*/
public void updateAdapter(List<String> suggestions) {
MatrixCursor cursor = new MatrixCursor(columns, suggestions.size());
int i = 0;
for (String suggestion : suggestions) {
String[] columnValues = new String[columns.length];
columnValues[INDEX_TITLE] = suggestion;
columnValues[INDEX_ID] = Integer.toString(i);
cursor.addRow(columnValues);
i++;
}
changeCursor(cursor);
}
/**
* Get the suggestion for a position
* @param position the position of the suggestion
* @return the suggestion
*/
public String getSuggestion(int position) {
return ((Cursor) getItem(position)).getString(INDEX_TITLE);
public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false));
}
@Override
public CharSequence convertToString(Cursor cursor) {
return cursor.getString(INDEX_TITLE);
public void onBindViewHolder(SuggestionItemHolder holder, int position) {
final SuggestionItem currentItem = getItem(position);
holder.updateFrom(currentItem);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) listener.onSuggestionItemSelected(currentItem);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (listener != null) listener.onSuggestionItemLongClick(currentItem);
return true;
}
});
}
private class ViewHolder {
private final TextView suggestionTitle;
private ViewHolder(View view) {
this.suggestionTitle = view.findViewById(android.R.id.text1);
private SuggestionItem getItem(int position) {
return items.get(position);
}
@Override
public int getItemCount() {
return items.size();
}
public boolean isEmpty() {
return getItemCount() == 0;
}
public static class SuggestionItemHolder extends RecyclerView.ViewHolder {
private final TextView itemSuggestionQuery;
private final ImageView suggestionIcon;
// Cache some ids, as they can potentially be constantly updated/recycled
private final int historyResId;
private final int searchResId;
private SuggestionItemHolder(View rootView) {
super(rootView);
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.history);
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.search);
}
private void updateFrom(SuggestionItem item) {
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
itemSuggestionQuery.setText(item.query);
}
private static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) {
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
int attributeResourceId = a.getResourceId(0, 0);
a.recycle();
return attributeResourceId;
}
}
}
}

View file

@ -19,6 +19,8 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
@ -52,6 +54,17 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
// Fragment LifeCycle
///////////////////////////////////////////////////////////////////////////
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser && activity != null) {
activity.getSupportActionBar()
.setTitle(R.string.tab_subscriptions);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@ -62,6 +75,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
activity.setTitle(R.string.tab_subscriptions);
if(useAsFrontPage) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
return inflater.inflate(R.layout.fragment_subscription, container, false);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,112 @@
package org.schabi.newpipe.playlist;
import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Localization;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
private final int thumbnailWidthPx;
private final int thumbnailHeightPx;
private final DisplayImageOptions imageOptions;
public interface OnSelectedListener {
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
void onStartDrag(PlayQueueItemHolder viewHolder);
}
private OnSelectedListener onItemClickListener;
public PlayQueueItemBuilder(final Context context) {
thumbnailWidthPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_width);
thumbnailHeightPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_height);
imageOptions = buildImageOptions(thumbnailWidthPx, thumbnailHeightPx);
}
public void setOnSelectedListener(OnSelectedListener listener) {
this.onItemClickListener = listener;
}
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
if (item.getDuration() > 0) {
holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
} else {
holder.itemDurationView.setVisibility(View.GONE);
}
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.selected(item, view);
}
}
});
holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.held(item, view);
return true;
}
return false;
}
});
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
holder.itemHandle.setOnTouchListener(getOnTouchListener(holder));
}
private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
return new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.performClick();
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
onItemClickListener.onStartDrag(holder);
}
return false;
}
};
}
private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
final BitmapProcessor bitmapProcessor = new BitmapProcessor() {
@Override
public Bitmap process(Bitmap bitmap) {
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
bitmap.recycle();
return resizedBitmap;
}
};
return new DisplayImageOptions.Builder()
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.bitmapConfig(Bitmap.Config.RGB_565) // Users won't be able to see much anyways
.preProcessor(bitmapProcessor)
.imageScaleType(ImageScaleType.EXACTLY)
.build();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -160,7 +160,7 @@ public class ErrorActivity extends AppCompatActivity {
key = k;
}
}
String[] el = new String[]{report.get(key)};
String[] el = new String[]{report.get(key).toString()};
Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);

View file

@ -19,8 +19,8 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
super.onCreate(savedInstanceState);
}
@Override

View file

@ -1,12 +1,175 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.KioskTranslator;
public class ContentSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.content_settings);
final ListPreference mainPageContentPref = (ListPreference) findPreference(getString(R.string.main_page_content_key));
mainPageContentPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValueO) {
final String newValue = newValueO.toString();
final String mainPrefOldValue =
defaultPreferences.getString(getString(R.string.main_page_content_key), "blank_page");
final String mainPrefOldSummary = getMainPagePrefSummery(mainPrefOldValue, mainPageContentPref);
if(newValue.equals(getString(R.string.kiosk_page_key))) {
SelectKioskFragment selectKioskFragment = new SelectKioskFragment();
selectKioskFragment.setOnSelectedLisener(new SelectKioskFragment.OnSelectedLisener() {
@Override
public void onKioskSelected(String kioskId, int service_id) {
defaultPreferences.edit()
.putInt(getString(R.string.main_page_selected_service), service_id).apply();
defaultPreferences.edit()
.putString(getString(R.string.main_page_selectd_kiosk_id), kioskId).apply();
String serviceName = "";
try {
serviceName = NewPipe.getService(service_id).getServiceInfo().name;
} catch (ExtractionException e) {
onError(e);
}
String kioskName = KioskTranslator.getTranslatedKioskName(kioskId,
getContext());
String summary =
String.format(getString(R.string.service_kiosk_string),
serviceName,
kioskName);
mainPageContentPref.setSummary(summary);
}
});
selectKioskFragment.setOnCancelListener(new SelectKioskFragment.OnCancelListener() {
@Override
public void onCancel() {
mainPageContentPref.setSummary(mainPrefOldSummary);
mainPageContentPref.setValue(mainPrefOldValue);
}
});
selectKioskFragment.show(getFragmentManager(), "select_kiosk");
} else if(newValue.equals(getString(R.string.channel_page_key))) {
SelectChannelFragment selectChannelFragment = new SelectChannelFragment();
selectChannelFragment.setOnSelectedLisener(new SelectChannelFragment.OnSelectedLisener() {
@Override
public void onChannelSelected(String url, String name, int service) {
defaultPreferences.edit()
.putInt(getString(R.string.main_page_selected_service), service).apply();
defaultPreferences.edit()
.putString(getString(R.string.main_page_selected_channel_url), url).apply();
defaultPreferences.edit()
.putString(getString(R.string.main_page_selected_channel_name), name).apply();
mainPageContentPref.setSummary(name);
}
});
selectChannelFragment.setOnCancelListener(new SelectChannelFragment.OnCancelListener() {
@Override
public void onCancel() {
mainPageContentPref.setSummary(mainPrefOldSummary);
mainPageContentPref.setValue(mainPrefOldValue);
}
});
selectChannelFragment.show(getFragmentManager(), "select_channel");
} else {
mainPageContentPref.setSummary(getMainPageSummeryByKey(newValue));
}
defaultPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, true).apply();
return true;
}
});
}
@Override
public void onResume() {
super.onResume();
final String mainPageContentKey = getString(R.string.main_page_content_key);
final Preference mainPagePref = findPreference(getString(R.string.main_page_content_key));
final String bpk = getString(R.string.blank_page_key);
if(defaultPreferences.getString(mainPageContentKey, bpk)
.equals(getString(R.string.channel_page_key))) {
mainPagePref.setSummary(defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error"));
} else if(defaultPreferences.getString(mainPageContentKey, bpk)
.equals(getString(R.string.kiosk_page_key))) {
try {
StreamingService service = NewPipe.getService(
defaultPreferences.getInt(
getString(R.string.main_page_selected_service), 0));
String kioskName = KioskTranslator.getTranslatedKioskName(
defaultPreferences.getString(
getString(R.string.main_page_selectd_kiosk_id), "Trending"),
getContext());
String summary =
String.format(getString(R.string.service_kiosk_string),
service.getServiceInfo().name,
kioskName);
mainPagePref.setSummary(summary);
} catch (Exception e) {
onError(e);
}
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private String getMainPagePrefSummery(final String mainPrefOldValue, final ListPreference mainPageContentPref) {
if(mainPrefOldValue.equals(getString(R.string.channel_page_key))) {
return defaultPreferences.getString(getString(R.string.main_page_selected_channel_name), "error");
} else {
return mainPageContentPref.getSummary().toString();
}
}
private int getMainPageSummeryByKey(final String key) {
if(key.equals(getString(R.string.blank_page_key))) {
return R.string.blank_page_summary;
} else if(key.equals(getString(R.string.kiosk_page_key))) {
return R.string.kiosk_page_summary;
} else if(key.equals(getString(R.string.feed_page_key))) {
return R.string.feed_page_summary;
} else if(key.equals(getString(R.string.subscription_page_key))) {
return R.string.subscription_page_summary;
} else if(key.equals(getString(R.string.channel_page_key))) {
return R.string.channel_page_summary;
}
return R.string.blank_page_summary;
}
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
protected boolean onError(Throwable e) {
final Activity activity = getActivity();
ErrorActivity.reportError(activity, e,
activity.getClass(),
null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
return true;
}
}

View file

@ -7,9 +7,8 @@ import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.util.Log;
import com.nononsenseapps.filepicker.FilePickerActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.FilePickerActivityHelper;
public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
@ -48,10 +47,10 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
}
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
Intent i = new Intent(getActivity(), FilePickerActivity.class)
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_DIR);
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {

View file

@ -0,0 +1,238 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import java.util.List;
import java.util.Vector;
import de.hdodenhof.circleimageview.CircleImageView;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* Created by Christian Schabesberger on 26.09.17.
* SelectChannelFragment.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class SelectChannelFragment extends DialogFragment {
private SelectChannelAdapter channelAdapter;
private SubscriptionService subscriptionService;
private ImageLoader imageLoader = ImageLoader.getInstance();
private ProgressBar progressBar;
private TextView emptyView;
private RecyclerView recyclerView;
private List<SubscriptionEntity> subscriptions = new Vector<>();
/*//////////////////////////////////////////////////////////////////////////
// Interfaces
//////////////////////////////////////////////////////////////////////////*/
public interface OnSelectedLisener {
void onChannelSelected(String url, String name, int service);
}
OnSelectedLisener onSelectedLisener = null;
public void setOnSelectedLisener(OnSelectedLisener listener) {
onSelectedLisener = listener;
}
public interface OnCancelListener {
void onCancel();
}
OnCancelListener onCancelListener = null;
public void setOnCancelListener(OnCancelListener listener) {
onCancelListener = listener;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.select_channel_fragment, container, false);
recyclerView = (RecyclerView) v.findViewById(R.id.items_list);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
channelAdapter = new SelectChannelAdapter();
recyclerView.setAdapter(channelAdapter);
progressBar = v.findViewById(R.id.progressBar);
emptyView = v.findViewById(R.id.empty_state_view);
progressBar.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.GONE);
subscriptionService = SubscriptionService.getInstance();
subscriptionService.getSubscription().toObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriptionObserver());
return v;
}
/*//////////////////////////////////////////////////////////////////////////
// Handle actions
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCancel(final DialogInterface dialogInterface) {
super.onCancel(dialogInterface);
if(onCancelListener != null) {
onCancelListener.onCancel();
}
}
private void clickedItem(int position) {
if(onSelectedLisener != null) {
SubscriptionEntity entry = subscriptions.get(position);
onSelectedLisener.onChannelSelected(entry.getUrl(), entry.getName(), entry.getServiceId());
}
dismiss();
}
/*//////////////////////////////////////////////////////////////////////////
// Item handling
//////////////////////////////////////////////////////////////////////////*/
private void displayChannels(List<SubscriptionEntity> subscriptions) {
this.subscriptions = subscriptions;
progressBar.setVisibility(View.GONE);
if(subscriptions.isEmpty()) {
emptyView.setVisibility(View.VISIBLE);
return;
}
recyclerView.setVisibility(View.VISIBLE);
}
private Observer<List<SubscriptionEntity>> getSubscriptionObserver() {
return new Observer<List<SubscriptionEntity>>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(List<SubscriptionEntity> subscriptions) {
displayChannels(subscriptions);
}
@Override
public void onError(Throwable exception) {
onError(exception);
}
@Override
public void onComplete() {
}
};
}
private class SelectChannelAdapter extends
RecyclerView.Adapter<SelectChannelAdapter.SelectChannelItemHolder> {
@Override
public SelectChannelItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View item = LayoutInflater.from(parent.getContext())
.inflate(R.layout.select_channel_item, parent, false);
return new SelectChannelItemHolder(item);
}
@Override
public void onBindViewHolder(SelectChannelItemHolder holder, final int position) {
SubscriptionEntity entry = subscriptions.get(position);
holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
clickedItem(position);
}
});
imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView, DISPLAY_IMAGE_OPTIONS);
}
@Override
public int getItemCount() {
return subscriptions.size();
}
public class SelectChannelItemHolder extends RecyclerView.ViewHolder {
public SelectChannelItemHolder(View v) {
super(v);
this.view = v;
thumbnailView = v.findViewById(R.id.itemThumbnailView);
titleView = v.findViewById(R.id.itemTitleView);
}
public View view;
public CircleImageView thumbnailView;
public TextView titleView;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
protected boolean onError(Throwable e) {
final Activity activity = getActivity();
ErrorActivity.reportError(activity, e,
activity.getClass(),
null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// ImageLoaderOptions
//////////////////////////////////////////////////////////////////////////*/
/**
* Base display options
*/
public static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.build();
}

View file

@ -0,0 +1,192 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.KioskTranslator;
import org.schabi.newpipe.util.ServiceIconMapper;
import java.util.List;
import java.util.Vector;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
/**
* Created by Christian Schabesberger on 09.10.17.
* SelectKioskFragment.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class SelectKioskFragment extends DialogFragment {
RecyclerView recyclerView = null;
SelectKioskAdapter selectKioskAdapter = null;
/*//////////////////////////////////////////////////////////////////////////
// Interfaces
//////////////////////////////////////////////////////////////////////////*/
public interface OnSelectedLisener {
void onKioskSelected(String kioskId, int service_id);
}
OnSelectedLisener onSelectedLisener = null;
public void setOnSelectedLisener(OnSelectedLisener listener) {
onSelectedLisener = listener;
}
public interface OnCancelListener {
void onCancel();
}
OnCancelListener onCancelListener = null;
public void setOnCancelListener(OnCancelListener listener) {
onCancelListener = listener;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.select_kiosk_fragment, container, false);
recyclerView = (RecyclerView) v.findViewById(R.id.items_list);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
try {
selectKioskAdapter = new SelectKioskAdapter();
} catch (Exception e) {
onError(e);
}
recyclerView.setAdapter(selectKioskAdapter);
return v;
}
/*//////////////////////////////////////////////////////////////////////////
// Handle actions
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCancel(final DialogInterface dialogInterface) {
super.onCancel(dialogInterface);
if(onCancelListener != null) {
onCancelListener.onCancel();
}
}
private void clickedItem(SelectKioskAdapter.Entry entry) {
if(onSelectedLisener != null) {
onSelectedLisener.onKioskSelected(entry.kioskId, entry.serviceId);
}
dismiss();
}
private class SelectKioskAdapter
extends RecyclerView.Adapter<SelectKioskAdapter.SelectKioskItemHolder> {
public class Entry {
public Entry (int i, int si, String ki, String kn){
icon = i; serviceId=si; kioskId=ki; kioskName = kn;
}
int icon;
int serviceId;
String kioskId;
String kioskName;
}
private List<Entry> kioskList = new Vector<>();
public SelectKioskAdapter()
throws Exception {
for(StreamingService service : NewPipe.getServices()) {
for(String kioskId : service.getKioskList().getAvailableKiosks()) {
String name = String.format(getString(R.string.service_kiosk_string),
service.getServiceInfo().name,
KioskTranslator.getTranslatedKioskName(kioskId, getContext()));
kioskList.add(new Entry(
//ServiceIconMapper.getIconResource(service.getServiceId()),
ServiceIconMapper.getIconResource(-1),
service.getServiceId(),
kioskId,
name));
}
}
}
public int getItemCount() {
//todo: uncommend this line on multyservice support
//return kioskList.size();
return 1;
}
public SelectKioskItemHolder onCreateViewHolder(ViewGroup parent, int type) {
View item = LayoutInflater.from(parent.getContext())
.inflate(R.layout.select_kiosk_item, parent, false);
return new SelectKioskItemHolder(item);
}
public class SelectKioskItemHolder extends RecyclerView.ViewHolder {
public SelectKioskItemHolder(View v) {
super(v);
this.view = v;
thumbnailView = v.findViewById(R.id.itemThumbnailView);
titleView = v.findViewById(R.id.itemTitleView);
}
public View view;
public ImageView thumbnailView;
public TextView titleView;
}
public void onBindViewHolder(SelectKioskItemHolder holder, final int position) {
final Entry entry = kioskList.get(position);
holder.titleView.setText(entry.kioskName);
holder.thumbnailView.setImageDrawable(ContextCompat.getDrawable(getContext(), entry.icon));
holder.view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
clickedItem(entry);
}
});
}
}
/*//////////////////////////////////////////////////////////////////////////
// Error
//////////////////////////////////////////////////////////////////////////*/
protected boolean onError(Throwable e) {
final Activity activity = getActivity();
ErrorActivity.reportError(activity, e,
activity.getClass(),
null,
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
"none", "", R.string.app_ui_crash));
return true;
}
}

View file

@ -19,7 +19,7 @@ public class AnimationUtils {
private static final boolean DEBUG = MainActivity.DEBUG;
public enum Type {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
}
public static void animateView(View view, boolean enterOrExit, long duration) {
@ -95,9 +95,16 @@ public class AnimationUtils {
case LIGHT_SCALE_AND_ALPHA:
animateLightScaleAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
break;
case SLIDE_AND_ALPHA:
animateSlideAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
break;
case LIGHT_SLIDE_AND_ALPHA:
animateLightSlideAndAlpha(view, enterOrExit, duration, delay, execOnEnd);
break;
}
}
/**
* Animate the background color of a view
*/
@ -237,4 +244,50 @@ public class AnimationUtils {
}).start();
}
}
private static void animateSlideAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
if (enterOrExit) {
view.setTranslationY(-view.getHeight());
view.setAlpha(0f);
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (execOnEnd != null) execOnEnd.run();
}
}).start();
} else {
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).translationY(-view.getHeight())
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
if (execOnEnd != null) execOnEnd.run();
}
}).start();
}
}
private static void animateLightSlideAndAlpha(final View view, boolean enterOrExit, long duration, long delay, final Runnable execOnEnd) {
if (enterOrExit) {
view.setTranslationY(-view.getHeight() / 2);
view.setAlpha(0f);
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(1f).translationY(0)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (execOnEnd != null) execOnEnd.run();
}
}).start();
} else {
view.animate().setInterpolator(new FastOutSlowInInterpolator()).alpha(0f).translationY(-view.getHeight() / 2)
.setDuration(duration).setStartDelay(delay).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
if (execOnEnd != null) execOnEnd.run();
}
}).start();
}
}
}

View file

@ -9,4 +9,7 @@ public class Constants {
public static final String KEY_QUERY = "key_query";
public static final String KEY_THEME_CHANGE = "key_theme_change";
public static final String KEY_MAIN_PAGE_CHANGE = "key_main_page_change";
public static final int NO_SERVICE_ID = -1;
}

View file

@ -26,6 +26,7 @@ import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.ListExtractor.NextItemsResult;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SearchResult;
@ -50,7 +51,14 @@ public final class ExtractorHelper {
//no instance
}
private static void checkServiceId(int serviceId) {
if(serviceId == Constants.NO_SERVICE_ID) {
throw new IllegalArgumentException("serviceId is NO_SERVICE_ID");
}
}
public static Single<SearchResult> searchFor(final int serviceId, final String query, final int pageNumber, final String searchLanguage, final SearchEngine.Filter filter) {
checkServiceId(serviceId);
return Single.fromCallable(new Callable<SearchResult>() {
@Override
public SearchResult call() throws Exception {
@ -61,6 +69,7 @@ public final class ExtractorHelper {
}
public static Single<NextItemsResult> getMoreSearchItems(final int serviceId, final String query, final int nextPageNumber, final String searchLanguage, final SearchEngine.Filter filter) {
checkServiceId(serviceId);
return searchFor(serviceId, query, nextPageNumber, searchLanguage, filter)
.map(new Function<SearchResult, NextItemsResult>() {
@Override
@ -71,6 +80,7 @@ public final class ExtractorHelper {
}
public static Single<List<String>> suggestionsFor(final int serviceId, final String query, final String searchLanguage) {
checkServiceId(serviceId);
return Single.fromCallable(new Callable<List<String>>() {
@Override
public List<String> call() throws Exception {
@ -80,6 +90,7 @@ public final class ExtractorHelper {
}
public static Single<StreamInfo> getStreamInfo(final int serviceId, final String url, boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<StreamInfo>() {
@Override
public StreamInfo call() throws Exception {
@ -89,6 +100,7 @@ public final class ExtractorHelper {
}
public static Single<ChannelInfo> getChannelInfo(final int serviceId, final String url, boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<ChannelInfo>() {
@Override
public ChannelInfo call() throws Exception {
@ -98,6 +110,7 @@ public final class ExtractorHelper {
}
public static Single<NextItemsResult> getMoreChannelItems(final int serviceId, final String url, final String nextStreamsUrl) {
checkServiceId(serviceId);
return Single.fromCallable(new Callable<NextItemsResult>() {
@Override
public NextItemsResult call() throws Exception {
@ -107,6 +120,7 @@ public final class ExtractorHelper {
}
public static Single<PlaylistInfo> getPlaylistInfo(final int serviceId, final String url, boolean forceLoad) {
checkServiceId(serviceId);
return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<PlaylistInfo>() {
@Override
public PlaylistInfo call() throws Exception {
@ -116,6 +130,7 @@ public final class ExtractorHelper {
}
public static Single<NextItemsResult> getMorePlaylistItems(final int serviceId, final String url, final String nextStreamsUrl) {
checkServiceId(serviceId);
return Single.fromCallable(new Callable<NextItemsResult>() {
@Override
public NextItemsResult call() throws Exception {
@ -124,6 +139,24 @@ public final class ExtractorHelper {
});
}
public static Single<KioskInfo> getKioskInfo(final int serviceId, final String url, final String contentCountry, boolean forceLoad) {
return checkCache(forceLoad, serviceId, url, Single.fromCallable(new Callable<KioskInfo>() {
@Override
public KioskInfo call() throws Exception {
return KioskInfo.getInfo(NewPipe.getService(serviceId), url, contentCountry);
}
}));
}
public static Single<NextItemsResult> getMoreKioskItems(final int serviceId, final String url, final String nextStreamsUrl) {
return Single.fromCallable(new Callable<NextItemsResult>() {
@Override
public NextItemsResult call() throws Exception {
return KioskInfo.getMoreItems(NewPipe.getService(serviceId), url, nextStreamsUrl);
}
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@ -133,6 +166,7 @@ public final class ExtractorHelper {
* and put the results in the cache.
*/
private static <I extends Info> Single<I> checkCache(boolean forceLoad, int serviceId, String url, Single<I> loadFromNetwork) {
checkServiceId(serviceId);
loadFromNetwork = loadFromNetwork.doOnSuccess(new Consumer<I>() {
@Override
public void accept(@NonNull I i) throws Exception {
@ -157,6 +191,7 @@ public final class ExtractorHelper {
* Default implementation uses the {@link InfoCache} to get cached results
*/
public static <I extends Info> Maybe<I> loadFromCache(final int serviceId, final String url) {
checkServiceId(serviceId);
return Maybe.defer(new Callable<MaybeSource<? extends I>>() {
@Override
public MaybeSource<? extends I> call() throws Exception {

View file

@ -0,0 +1,17 @@
package org.schabi.newpipe.util;
import android.os.Bundle;
import org.schabi.newpipe.R;
public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
if(ThemeHelper.isLightThemeSelected(this)) {
this.setTheme(R.style.FilePickerThemeLight);
} else {
this.setTheme(R.style.FilePickerThemeDark);
}
super.onCreate(savedInstanceState);
}
}

View file

@ -0,0 +1,38 @@
package org.schabi.newpipe.util;
import android.content.Context;
import org.schabi.newpipe.R;
/**
* Created by Chrsitian Schabesberger on 28.09.17.
* KioskTranslator.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class KioskTranslator {
public static String getTranslatedKioskName(String kioskId, Context c) {
switch(kioskId) {
case "Trending":
return c.getString(R.string.trending);
case "Top 50":
return c.getString(R.string.top_50);
case "New & hot":
return c.getString(R.string.new_and_hot);
default:
return kioskId;
}
}
}

View file

@ -0,0 +1,43 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.graphics.PointF;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
public class LayoutManagerSmoothScroller extends LinearLayoutManager {
public LayoutManagerSmoothScroller(Context context) {
super(context, VERTICAL, false);
}
public LayoutManagerSmoothScroller(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private class TopSnappedSmoothScroller extends LinearSmoothScroller {
public TopSnappedSmoothScroller(Context context) {
super(context);
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return LayoutManagerSmoothScroller.this
.computeScrollVectorForPosition(targetPosition);
}
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;
}
}
}

View file

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

View file

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

View file

@ -1,11 +1,14 @@
package org.schabi.newpipe.util;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import com.nostra13.universalimageloader.core.ImageLoader;
@ -14,23 +17,22 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.about.AboutActivity;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistFragment;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.history.HistoryActivity;
import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.BasePlayer;
import org.schabi.newpipe.player.VideoPlayer;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.settings.SettingsActivity;
import java.util.ArrayList;
@SuppressWarnings({"unused", "WeakerAccess"})
public class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
@ -38,46 +40,41 @@ public class NavigationHelper {
/*//////////////////////////////////////////////////////////////////////////
// Players
//////////////////////////////////////////////////////////////////////////*/
public static Intent getPlayerIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue,
final String quality) {
Intent intent = new Intent(context, targetClazz)
.putExtra(VideoPlayer.PLAY_QUEUE, playQueue);
if (quality != null) intent.putExtra(VideoPlayer.PLAYBACK_QUALITY, quality);
public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, StreamInfo info, int selectedStreamIndex) {
Intent mIntent = new Intent(context, targetClazz)
.putExtra(BasePlayer.VIDEO_TITLE, info.name)
.putExtra(BasePlayer.VIDEO_URL, info.url)
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name)
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamIndex)
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)))
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, ListHelper.getHighestQualityAudio(info.audio_streams));
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L);
return mIntent;
return intent;
}
public static Intent getOpenVideoPlayerIntent(Context context, Class targetClazz, VideoPlayer instance) {
return new Intent(context, targetClazz)
.putExtra(BasePlayer.VIDEO_TITLE, instance.getVideoTitle())
.putExtra(BasePlayer.VIDEO_URL, instance.getVideoUrl())
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, instance.getVideoThumbnailUrl())
.putExtra(BasePlayer.CHANNEL_NAME, instance.getUploaderName())
.putExtra(VideoPlayer.INDEX_SEL_VIDEO_STREAM, instance.getSelectedStreamIndex())
.putExtra(VideoPlayer.VIDEO_STREAMS_LIST, instance.getVideoStreamsList())
.putExtra(VideoPlayer.VIDEO_ONLY_AUDIO_STREAM, instance.getAudioStream())
.putExtra(BasePlayer.START_POSITION, instance.getPlayer().getCurrentPosition())
.putExtra(BasePlayer.PLAYBACK_SPEED, instance.getPlaybackSpeed());
public static Intent getPlayerIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue) {
return getPlayerIntent(context, targetClazz, playQueue, null);
}
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info) {
return getOpenBackgroundPlayerIntent(context, info, info.audio_streams.get(ListHelper.getDefaultAudioFormat(context, info.audio_streams)));
public static Intent getPlayerEnqueueIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue) {
return getPlayerIntent(context, targetClazz, playQueue)
.putExtra(BasePlayer.APPEND_ONLY, true);
}
public static Intent getOpenBackgroundPlayerIntent(Context context, StreamInfo info, AudioStream audioStream) {
Intent mIntent = new Intent(context, BackgroundPlayer.class)
.putExtra(BasePlayer.VIDEO_TITLE, info.name)
.putExtra(BasePlayer.VIDEO_URL, info.url)
.putExtra(BasePlayer.VIDEO_THUMBNAIL_URL, info.thumbnail_url)
.putExtra(BasePlayer.CHANNEL_NAME, info.uploader_name)
.putExtra(BackgroundPlayer.AUDIO_STREAM, audioStream);
if (info.start_position > 0) mIntent.putExtra(BasePlayer.START_POSITION, info.start_position * 1000L);
return mIntent;
public static Intent getPlayerIntent(final Context context,
final Class targetClazz,
final PlayQueue playQueue,
final int repeatMode,
final float playbackSpeed,
final float playbackPitch,
final String playbackQuality) {
return getPlayerIntent(context, targetClazz, playQueue, playbackQuality)
.putExtra(BasePlayer.REPEAT_MODE, repeatMode)
.putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
.putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch);
}
/*//////////////////////////////////////////////////////////////////////////
@ -91,7 +88,7 @@ public class NavigationHelper {
if (!popped) openMainFragment(fragmentManager);
}
private static void openMainFragment(FragmentManager fragmentManager) {
public static void openMainFragment(FragmentManager fragmentManager) {
InfoCache.getInstance().trimCache();
fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
@ -161,6 +158,15 @@ public class NavigationHelper {
.commit();
}
public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId)
throws ExtractionException {
fragmentManager.beginTransaction()
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId))
.addToBackStack(null)
.commit();
}
/*//////////////////////////////////////////////////////////////////////////
// Through Intents
//////////////////////////////////////////////////////////////////////////*/
@ -228,13 +234,17 @@ public class NavigationHelper {
// Link handling
//////////////////////////////////////////////////////////////////////////*/
public static void openByLink(Context context, String url) throws Exception {
Intent intentByLink = getIntentByLink(context, url);
if (intentByLink == null)
throw new NullPointerException("getIntentByLink(context = [" + context + "], url = [" + url + "]) returned null");
public static boolean openByLink(Context context, String url) {
Intent intentByLink;
try {
intentByLink = getIntentByLink(context, url);
} catch (ExtractionException e) {
return false;
}
intentByLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intentByLink.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intentByLink);
return true;
}
private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) {
@ -245,14 +255,20 @@ public class NavigationHelper {
return mIntent;
}
private static Intent getIntentByLink(Context context, String url) throws Exception {
StreamingService service = NewPipe.getServiceByUrl(url);
public static Intent getIntentByLink(Context context, String url) throws ExtractionException {
return getIntentByLink(context, NewPipe.getServiceByUrl(url), url);
}
public static Intent getIntentByLink(Context context, StreamingService service, String url) throws ExtractionException {
if (service != ServiceList.YouTube.getService()) {
throw new ExtractionException("Service not supported at the moment");
}
int serviceId = service.getServiceId();
StreamingService.LinkType linkType = service.getLinkTypeByUrl(url);
if (linkType == StreamingService.LinkType.NONE) {
throw new Exception("Url not known to service. service=" + serviceId + " url=" + url);
throw new ExtractionException("Url not known to service. service=" + serviceId + " url=" + url);
}
url = getCleanUrl(service, url, linkType);
@ -268,7 +284,7 @@ public class NavigationHelper {
return rIntent;
}
private static String getCleanUrl(StreamingService service, String dirtyUrl, StreamingService.LinkType linkType) throws Exception {
private static String getCleanUrl(StreamingService service, String dirtyUrl, StreamingService.LinkType linkType) throws ExtractionException {
switch (linkType) {
case STREAM:
return service.getStreamUrlIdHandler().cleanUrl(dirtyUrl);
@ -281,4 +297,55 @@ public class NavigationHelper {
}
return null;
}
private static Uri openMarketUrl(String packageName) {
return Uri.parse("market://details")
.buildUpon()
.appendQueryParameter("id", packageName)
.build();
}
private static Uri getGooglePlayUrl(String packageName) {
return Uri.parse("https://play.google.com/store/apps/details")
.buildUpon()
.appendQueryParameter("id", packageName)
.build();
}
private static void installApp(Context context, String packageName) {
try {
// Try market:// scheme
context.startActivity(new Intent(Intent.ACTION_VIEW, openMarketUrl(packageName)));
} catch (ActivityNotFoundException e) {
// Fall back to google play URL (don't worry F-Droid can handle it :)
context.startActivity(new Intent(Intent.ACTION_VIEW, getGooglePlayUrl(packageName)));
}
}
/**
* Start an activity to install Kore
* @param context the context
*/
public static void installKore(Context context) {
installApp(context, context.getString(R.string.kore_package));
}
/**
* Start Kore app to show a video on Kodi
*
* For a list of supported urls see the
* <a href="https://github.com/xbmc/Kore/blob/master/app/src/main/AndroidManifest.xml">
* Kore source code
* </a>.
*
* @param context the context to use
* @param videoURL the url to the video
*/
public static void playWithKore(Context context, Uri videoURL) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(context.getString(R.string.kore_package));
intent.setData(videoURL);
context.startActivity(intent);
}
}

View file

@ -0,0 +1,35 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
/**
* Created by Chrsitian Schabesberger on 09.10.17.
* ServiceIconMapper.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ServiceIconMapper {
public static int getIconResource(int service_id) {
switch(service_id) {
case 0:
return R.drawable.youtube;
case 1:
return R.drawable.soud_cloud;
default:
return R.drawable.service;
}
}
}

View file

@ -21,6 +21,7 @@ package org.schabi.newpipe.util;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@ -29,6 +30,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.MainActivity;
import java.io.File;
@ -110,6 +112,7 @@ public class StateSaver {
/**
* Try to restore the state from memory and disk, using the {@link StateSaver.WriteRead#readFrom(Queue)} from the writeRead.
*/
@Nullable
private static SavedState tryToRestore(@NonNull SavedState savedState, @NonNull WriteRead writeRead) {
if (MainActivity.DEBUG) {
Log.d(TAG, "tryToRestore() called with: savedState = [" + savedState + "], writeRead = [" + writeRead + "]");
@ -117,7 +120,7 @@ public class StateSaver {
FileInputStream fileInputStream = null;
try {
Queue<Object> savedObjects = stateObjectsHolder.remove(savedState.prefixFileSaved);
Queue<Object> savedObjects = stateObjectsHolder.remove(savedState.getPrefixFileSaved());
if (savedObjects != null) {
writeRead.readFrom(savedObjects);
if (MainActivity.DEBUG) {
@ -126,8 +129,13 @@ public class StateSaver {
return savedState;
}
File file = new File(savedState.pathFileSaved);
if (!file.exists()) return null;
File file = new File(savedState.getPathFileSaved());
if (!file.exists()) {
if(MainActivity.DEBUG) {
Log.d(TAG, "Cache file doesn't exist: " + file.getAbsolutePath());
}
return null;
}
fileInputStream = new FileInputStream(file);
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
@ -139,7 +147,7 @@ public class StateSaver {
return savedState;
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "Failed to restore state", e);
} finally {
if (fileInputStream != null) {
try {
@ -154,10 +162,17 @@ public class StateSaver {
/**
* @see #tryToSave(boolean, String, String, WriteRead)
*/
@Nullable
public static SavedState tryToSave(boolean isChangingConfig, @Nullable SavedState savedState, Bundle outState, WriteRead writeRead) {
String currentSavedPrefix = savedState == null || TextUtils.isEmpty(savedState.prefixFileSaved)
? System.nanoTime() - writeRead.hashCode() + ""
: savedState.prefixFileSaved;
@NonNull
String currentSavedPrefix;
if (savedState == null || TextUtils.isEmpty(savedState.getPrefixFileSaved())) {
// Generate unique prefix
currentSavedPrefix = System.nanoTime() - writeRead.hashCode() + "";
} else {
// Reuse prefix
currentSavedPrefix = savedState.getPrefixFileSaved();
}
savedState = tryToSave(isChangingConfig, currentSavedPrefix, writeRead.generateSuffix(), writeRead);
if (savedState != null) {
@ -173,22 +188,33 @@ public class StateSaver {
* to the file with the name of prefixFileName + suffixFileName, in a cache folder got from the {@link #init(Context)}.
* <p>
* It checks if the file already exists and if it does, just return the path, so a good way to save is:
* <li> A fixed prefix for the file
* <li> A changing suffix
* <ul>
* <li> A fixed prefix for the file</li>
* <li> A changing suffix</li>
* </ul>
*
* @param isChangingConfig
* @param prefixFileName
* @param suffixFileName
* @param writeRead
*/
@Nullable
private static SavedState tryToSave(boolean isChangingConfig, final String prefixFileName, String suffixFileName, WriteRead writeRead) {
if (MainActivity.DEBUG) {
Log.d(TAG, "tryToSave() called with: isChangingConfig = [" + isChangingConfig + "], prefixFileName = [" + prefixFileName + "], suffixFileName = [" + suffixFileName + "], writeRead = [" + writeRead + "]");
}
Queue<Object> savedObjects = new LinkedList<>();
LinkedList<Object> savedObjects = new LinkedList<>();
writeRead.writeTo(savedObjects);
if (isChangingConfig) {
if (savedObjects.size() > 0) {
stateObjectsHolder.put(prefixFileName, savedObjects);
return new SavedState(prefixFileName, "");
} else return null;
} else {
if(MainActivity.DEBUG) Log.d(TAG, "Nothing to save");
return null;
}
}
FileOutputStream fileOutputStream = null;
@ -197,8 +223,12 @@ public class StateSaver {
if (!cacheDir.exists()) throw new RuntimeException("Cache dir does not exist > " + cacheDirPath);
cacheDir = new File(cacheDir, CACHE_DIR_NAME);
if (!cacheDir.exists()) {
boolean mkdirResult = cacheDir.mkdir();
if (!mkdirResult) return null;
if(!cacheDir.mkdir()) {
if(BuildConfig.DEBUG) {
Log.e(TAG, "Failed to create cache directory " + cacheDir.getAbsolutePath());
}
return null;
}
}
if (TextUtils.isEmpty(suffixFileName)) suffixFileName = ".cache";
@ -214,7 +244,9 @@ public class StateSaver {
return name.contains(prefixFileName);
}
});
for (File file1 : files) file1.delete();
for (File fileToDelete : files) {
fileToDelete.delete();
}
}
fileOutputStream = new FileOutputStream(file);
@ -223,7 +255,7 @@ public class StateSaver {
return new SavedState(prefixFileName, file.getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "Failed to save state", e);
} finally {
if (fileOutputStream != null) {
try {
@ -241,11 +273,11 @@ public class StateSaver {
public static void onDestroy(SavedState savedState) {
if (MainActivity.DEBUG) Log.d(TAG, "onDestroy() called with: savedState = [" + savedState + "]");
if (savedState != null && !TextUtils.isEmpty(savedState.pathFileSaved)) {
stateObjectsHolder.remove(savedState.prefixFileSaved);
if (savedState != null && !TextUtils.isEmpty(savedState.getPathFileSaved())) {
stateObjectsHolder.remove(savedState.getPrefixFileSaved());
try {
//noinspection ResultOfMethodCallIgnored
new File(savedState.pathFileSaved).delete();
new File(savedState.getPathFileSaved()).delete();
} catch (Exception ignored) {
}
}
@ -271,9 +303,12 @@ public class StateSaver {
// Inner
//////////////////////////////////////////////////////////////////////////*/
/**
* Information about the saved state on the disk
*/
public static class SavedState implements Parcelable {
public String prefixFileSaved;
public String pathFileSaved;
private final String prefixFileSaved;
private final String pathFileSaved;
public SavedState(String prefixFileSaved, String pathFileSaved) {
this.prefixFileSaved = prefixFileSaved;
@ -287,7 +322,7 @@ public class StateSaver {
@Override
public String toString() {
return prefixFileSaved + " > " + pathFileSaved;
return getPrefixFileSaved() + " > " + getPathFileSaved();
}
@Override
@ -313,6 +348,22 @@ public class StateSaver {
return new SavedState[size];
}
};
/**
* Get the prefix of the saved file
* @return the file prefix
*/
public String getPrefixFileSaved() {
return prefixFileSaved;
}
/**
* Get the path to the saved file
* @return the path to the saved file
*/
public String getPathFileSaved() {
return pathFileSaved;
}
}

View file

@ -101,6 +101,18 @@ public class DownloadManagerImpl implements DownloadManager {
}
/**
* Sort a list of mission by its timestamp. Oldest first
* @param missions the missions to sort
*/
static void sortByTimestamp(List<DownloadMission> missions) {
Collections.sort(missions, new Comparator<DownloadMission>() {
@Override
public int compare(DownloadMission o1, DownloadMission o2) {
return Long.valueOf(o1.timestamp).compareTo(o2.timestamp);
}
});
}
/**
* Loads finished missions from the data source
@ -111,12 +123,8 @@ public class DownloadManagerImpl implements DownloadManager {
finishedMissions = new ArrayList<>();
}
// Ensure its sorted
Collections.sort(finishedMissions, new Comparator<DownloadMission>() {
@Override
public int compare(DownloadMission o1, DownloadMission o2) {
return (int) (o1.timestamp - o2.timestamp);
}
});
sortByTimestamp(finishedMissions);
mMissions.ensureCapacity(mMissions.size() + finishedMissions.size());
for (DownloadMission mission : finishedMissions) {
File downloadedFile = mission.getDownloadedFile();

View file

@ -6,8 +6,8 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
@ -15,7 +15,6 @@ import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.PermissionChecker;
import android.util.Log;
import android.widget.Toast;
@ -96,12 +95,12 @@ public class DownloadManagerService extends Service {
openDownloadListIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
Drawable icon = ContextCompat.getDrawable(this, R.mipmap.ic_launcher);
Bitmap iconBitmap = BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher);
Builder builder = new Builder(this, getString(R.string.notification_channel_id))
.setContentIntent(pendingIntent)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon(((BitmapDrawable) icon).getBitmap())
.setLargeIcon(iconBitmap)
.setContentTitle(getString(R.string.msg_running))
.setContentText(getString(R.string.msg_running_detail));