diff --git a/app/build.gradle b/app/build.gradle index 37ec0e38e..3d122e516 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,6 +31,7 @@ android { } dependencies { + testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:24.1.1' compile 'com.android.support:support-v4:24.1.1' compile 'com.android.support:design:24.1.1' diff --git a/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorDefaultTest.java b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorDefaultTest.java index 9428a3605..cc163dbb9 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorDefaultTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/extractor/youtube/YoutubeStreamExtractorDefaultTest.java @@ -79,6 +79,10 @@ public class YoutubeStreamExtractorDefaultTest extends AndroidTestCase { assertTrue(extractor.getUploadDate().length() > 0); } + public void testGetChannelUrl() throws ParsingException { + assertTrue(extractor.getChannelUrl().length() > 0); + } + public void testGetThumbnailUrl() throws ParsingException { assertTrue(extractor.getThumbnailUrl(), extractor.getThumbnailUrl().contains(HTTPS)); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3692b6328..bffefd177 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,10 +16,10 @@ android:logo="@mipmap/ic_launcher" android:theme="@style/AppTheme" tools:ignore="AllowBackup"> + + android:name=".MainActivity" + android:label="@string/app_name"> @@ -27,12 +27,12 @@ + android:value=".MainActivity" /> @@ -80,12 +80,14 @@ android:name=".player.PlayVideoActivity" android:configChanges="orientation|keyboardHidden|screenSize" android:theme="@style/VideoPlayerTheme" - tools:ignore="UnusedAttribute"/> + tools:ignore="UnusedAttribute" /> + - + + + + android:exported="false" + android:label="@string/background_player_name" /> + - + - + android:launchMode="singleTask" + android:theme="@style/AppTheme" /> - + + + - + + \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java index ea76b46f2..a097e087c 100644 --- a/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java +++ b/app/src/main/java/org/schabi/newpipe/ActivityCommunicator.java @@ -43,7 +43,7 @@ public class ActivityCommunicator { public volatile Bitmap backgroundPlayerThumbnail; // Sent from any activity to ErrorActivity. - public volatile List errorList; + public volatile List errorList; public volatile Class returnActivity; public volatile ErrorActivity.ErrorInfo errorInfo; } diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index c56893fc8..f0604f16f 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -2,12 +2,12 @@ package org.schabi.newpipe; import android.app.Application; import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; +import org.schabi.newpipe.settings.SettingsActivity; + import info.guardianproject.netcipher.NetCipher; import info.guardianproject.netcipher.proxy.OrbotHelper; diff --git a/app/src/main/java/org/schabi/newpipe/ChannelActivity.java b/app/src/main/java/org/schabi/newpipe/ChannelActivity.java new file mode 100644 index 000000000..a58182a5e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ChannelActivity.java @@ -0,0 +1,248 @@ +package org.schabi.newpipe; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.support.design.widget.CollapsingToolbarLayout; +import android.support.design.widget.FloatingActionButton; +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.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.Toast; + +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.detail.VideoItemDetailActivity; +import org.schabi.newpipe.detail.VideoItemDetailFragment; +import org.schabi.newpipe.extractor.ChannelExtractor; +import org.schabi.newpipe.extractor.ChannelInfo; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.info_list.InfoListAdapter; + +import java.io.IOException; + +public class ChannelActivity extends AppCompatActivity { + + + private static final String TAG = ChannelActivity.class.toString(); + private View rootView = null; + + // intent const + public static final String CHANNEL_URL = "channel_url"; + public static final String SERVICE_ID = "service_id"; + + private int serviceId = -1; + private String channelUrl = ""; + private int pageNumber = 0; + private boolean hasNextPage = true; + private boolean isLoading = false; + + private ImageLoader imageLoader = ImageLoader.getInstance(); + private InfoListAdapter infoListAdapter = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_channel); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + rootView = findViewById(R.id.rootView); + setSupportActionBar(toolbar); + Intent i = getIntent(); + channelUrl = i.getStringExtra(CHANNEL_URL); + serviceId = i.getIntExtra(SERVICE_ID, -1); + + infoListAdapter = new InfoListAdapter(this, rootView); + RecyclerView recyclerView = (RecyclerView) findViewById(R.id.channel_streams_view); + final LinearLayoutManager layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(infoListAdapter); + infoListAdapter.setOnItemSelectedListener(new InfoListAdapter.OnItemSelectedListener() { + @Override + public void selected(String url) { + Intent detailIntent = new Intent(ChannelActivity.this, VideoItemDetailActivity.class); + detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, url); + detailIntent.putExtra( + VideoItemDetailFragment.STREAMING_SERVICE, serviceId); + startActivity(detailIntent); + } + }); + + // detect if list has ben scrolled to the bottom + recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + int pastVisiblesItems, visibleItemCount, totalItemCount; + super.onScrolled(recyclerView, dx, dy); + if(dy > 0) //check for scroll down + { + visibleItemCount = layoutManager.getChildCount(); + totalItemCount = layoutManager.getItemCount(); + pastVisiblesItems = layoutManager.findFirstVisibleItemPosition(); + + if ( (visibleItemCount + pastVisiblesItems) >= totalItemCount + && !isLoading + && hasNextPage) + { + pageNumber++; + requestData(true); + } + } + } + }); + + requestData(false); + } + + + + private void updateUi(final ChannelInfo info) { + CollapsingToolbarLayout ctl = (CollapsingToolbarLayout) findViewById(R.id.channel_toolbar_layout); + ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar); + ImageView channelBanner = (ImageView) findViewById(R.id.channel_banner_image); + FloatingActionButton feedButton = (FloatingActionButton) findViewById(R.id.channel_rss_fab); + ImageView avatarView = (ImageView) findViewById(R.id.channel_avatar_view); + ImageView haloView = (ImageView) findViewById(R.id.channel_avatar_halo); + + progressBar.setVisibility(View.GONE); + + if(info.channel_name != null && !info.channel_name.isEmpty()) { + ctl.setTitle(info.channel_name); + } + + if(info.banner_url != null && !info.banner_url.isEmpty()) { + imageLoader.displayImage(info.banner_url, channelBanner, + new ImageErrorLoadingListener(this, rootView ,info.service_id)); + } + + if(info.avatar_url != null && !info.avatar_url.isEmpty()) { + avatarView.setVisibility(View.VISIBLE); + haloView.setVisibility(View.VISIBLE); + imageLoader.displayImage(info.avatar_url, avatarView, + new ImageErrorLoadingListener(this, rootView ,info.service_id)); + } + + if(info.feed_url != null && !info.feed_url.isEmpty()) { + feedButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.d(TAG, info.feed_url); + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(info.feed_url)); + startActivity(i); + } + }); + } else { + feedButton.setVisibility(View.GONE); + } + } + + private void addVideos(final ChannelInfo info) { + infoListAdapter.addStreamItemList(info.related_streams); + } + + private void postNewErrorToast(Handler h, final int stringResource) { + h.post(new Runnable() { + @Override + public void run() { + Toast.makeText(ChannelActivity.this, + stringResource, Toast.LENGTH_LONG).show(); + } + }); + } + + private void requestData(final boolean onlyVideos) { + // start processing + isLoading = true; + Thread channelExtractorThread = new Thread(new Runnable() { + Handler h = new Handler(); + + @Override + public void run() { + StreamingService service = null; + try { + service = ServiceList.getService(serviceId); + ChannelExtractor extractor = service.getChannelExtractorInstance( + channelUrl, pageNumber, new Downloader()); + + final ChannelInfo info = ChannelInfo.getInfo(extractor, new Downloader()); + + + h.post(new Runnable() { + @Override + public void run() { + isLoading = false; + if(!onlyVideos) { + updateUi(info); + } + hasNextPage = info.hasNextPage; + addVideos(info); + } + }); + + // look for non critical errors during extraction + if(info != null && + !info.errors.isEmpty()) { + Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:"); + for (Throwable e : info.errors) { + e.printStackTrace(); + Log.e(TAG, "------"); + } + + View rootView = findViewById(android.R.id.content); + ErrorActivity.reportError(h, ChannelActivity.this, + info.errors, null, rootView, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, channelUrl, 0 /* no message for the user */)); + } + } catch(IOException ioe) { + postNewErrorToast(h, R.string.network_error); + ioe.printStackTrace(); + } catch(ParsingException pe) { + ErrorActivity.reportError(h, ChannelActivity.this, pe, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, channelUrl, R.string.parsing_error)); + h.post(new Runnable() { + @Override + public void run() { + ChannelActivity.this.finish(); + } + }); + pe.printStackTrace(); + } catch(ExtractionException ex) { + ErrorActivity.reportError(h, ChannelActivity.this, ex, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, channelUrl, R.string.parsing_error)); + h.post(new Runnable() { + @Override + public void run() { + ChannelActivity.this.finish(); + } + }); + ex.printStackTrace(); + } catch(Exception e) { + ErrorActivity.reportError(h, ChannelActivity.this, e, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, channelUrl, R.string.general_error)); + h.post(new Runnable() { + @Override + public void run() { + ChannelActivity.this.finish(); + } + }); + e.printStackTrace(); + } + } + }); + + channelExtractorThread.start(); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/Downloader.java b/app/src/main/java/org/schabi/newpipe/Downloader.java index 833891762..04098a4d7 100644 --- a/app/src/main/java/org/schabi/newpipe/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/Downloader.java @@ -5,10 +5,12 @@ import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import javax.net.ssl.HttpsURLConnection; -import info.guardianproject.netcipher.NetCipher; /** * Created by Christian Schabesberger on 28.01.16. @@ -40,10 +42,26 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader { * @param language the language (usually a 2-character code) to set as the preferred language * @return the contents of the specified text file*/ public String download(String siteUrl, String language) throws IOException { + Map requestProperties = new HashMap<>(); + requestProperties.put("Accept-Language", language); + return download(siteUrl, requestProperties); + } + + + /**Download the text file at the supplied URL as in download(String), + * but set the HTTP header field "Accept-Language" to the supplied string. + * @param siteUrl the URL of the text file to return the contents of + * @param customProperties set request header properties + * @return the contents of the specified text file + * @throws IOException*/ + public String download(String siteUrl, Map customProperties) throws IOException { URL url = new URL(siteUrl); HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); - //HttpsURLConnection con = NetCipher.getHttpsURLConnection(url); - con.setRequestProperty("Accept-Language", language); + Iterator it = customProperties.entrySet().iterator(); + while(it.hasNext()) { + Map.Entry pair = (Map.Entry)it.next(); + con.setRequestProperty((String)pair.getKey(), (String)pair.getValue()); + } return dl(con); } diff --git a/app/src/main/java/org/schabi/newpipe/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/ErrorActivity.java index ec43730dd..a992f0fd1 100644 --- a/app/src/main/java/org/schabi/newpipe/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ErrorActivity.java @@ -79,16 +79,19 @@ public class ErrorActivity extends AppCompatActivity { public static final int GET_SUGGESTIONS = 2; public static final int SOMETHING_ELSE = 3; public static final int USER_REPORT = 4; + public static final int LOAD_IMAGE = 5; public static final String SEARCHED_STRING = "searched"; public static final String REQUESTED_STREAM_STRING = "requested stream"; public static final String GET_SUGGESTIONS_STRING = "get suggestions"; public static final String SOMETHING_ELSE_STRING = "something"; public static final String USER_REPORT_STRING = "user report"; + public static final String LOAD_IMAGE_STRING = "load image"; + public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME; - private List errorList; + private List errorList; private ErrorInfo errorInfo; private Class returnActivity; private String currentTimeStamp; @@ -102,7 +105,7 @@ public class ErrorActivity extends AppCompatActivity { private TextView infoView; private TextView errorMessageView; - public static void reportError(final Context context, final List el, + public static void reportError(final Context context, final List el, final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) { if (rootView != null) { @@ -129,9 +132,9 @@ public class ErrorActivity extends AppCompatActivity { } } - public static void reportError(final Context context, final Exception e, + public static void reportError(final Context context, final Throwable e, final Class returnAcitivty, View rootView, final ErrorInfo errorInfo) { - List el = null; + List el = null; if(e != null) { el = new Vector<>(); el.add(e); @@ -140,10 +143,10 @@ public class ErrorActivity extends AppCompatActivity { } // async call - public static void reportError(Handler handler, final Context context, final Exception e, + public static void reportError(Handler handler, final Context context, final Throwable e, final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) { - List el = null; + List el = null; if(e != null) { el = new Vector<>(); el.add(e); @@ -152,7 +155,7 @@ public class ErrorActivity extends AppCompatActivity { } // async call - public static void reportError(Handler handler, final Context context, final List el, + public static void reportError(Handler handler, final Context context, final List el, final Class returnAcitivty, final View rootView, final ErrorInfo errorInfo) { handler.post(new Runnable() { @Override @@ -171,7 +174,7 @@ public class ErrorActivity extends AppCompatActivity { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(R.string.error_report_title); actionBar.setDisplayShowTitleEnabled(true); - } catch (Exception e) { + } catch (Throwable e) { Log.e(TAG, "Error turing exception handling"); e.printStackTrace(); } @@ -252,10 +255,10 @@ public class ErrorActivity extends AppCompatActivity { return sw.getBuffer().toString(); } - private String formErrorText(List el) { + private String formErrorText(List el) { String text = ""; if(el != null) { - for (Exception e : el) { + for (Throwable e : el) { text += "-------------------------------------\n" + getStackTrace(e); } @@ -273,7 +276,7 @@ public class ErrorActivity extends AppCompatActivity { returnActivity.isAssignableFrom(Activity.class)) { intent = new Intent(this, returnActivity); } else { - intent = new Intent(this, VideoItemListActivity.class); + intent = new Intent(this, MainActivity.class); } intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); NavUtils.navigateUpTo(this, intent); @@ -313,7 +316,7 @@ public class ErrorActivity extends AppCompatActivity { JSONArray exceptionArray = new JSONArray(); if(errorList != null) { - for (Exception e : errorList) { + for (Throwable e : errorList) { exceptionArray.put(getStackTrace(e)); } } @@ -322,7 +325,7 @@ public class ErrorActivity extends AppCompatActivity { errorObject.put("user_comment", userCommentBox.getText().toString()); return errorObject.toString(3); - } catch (Exception e) { + } catch (Throwable e) { Log.e(TAG, "Error while erroring: Could not build json"); e.printStackTrace(); } @@ -342,6 +345,8 @@ public class ErrorActivity extends AppCompatActivity { return SOMETHING_ELSE_STRING; case USER_REPORT: return USER_REPORT_STRING; + case LOAD_IMAGE: + return LOAD_IMAGE_STRING; default: return "Your description is in another castle."; } @@ -390,7 +395,7 @@ public class ErrorActivity extends AppCompatActivity { ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip) + "0.0"; - } catch(Exception e) { + } catch(Throwable e) { Log.d(TAG, "Error while error: could not get iprange"); e.printStackTrace(); } finally { diff --git a/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java b/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java new file mode 100644 index 000000000..0447ed647 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java @@ -0,0 +1,65 @@ +package org.schabi.newpipe; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.view.View; + +import com.nostra13.universalimageloader.core.assist.FailReason; +import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; + +import org.schabi.newpipe.ErrorActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ServiceList; + +/** + * Created by Christian Schabesberger on 01.08.16. + * + * Copyright (C) Christian Schabesberger 2015 + * StreamInfoItemViewCreator.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 . + */ + +public class ImageErrorLoadingListener implements ImageLoadingListener { + + private int serviceId = -1; + private Activity activity = null; + private View rootView = null; + + public ImageErrorLoadingListener(Activity activity, View rootView, int serviceId) { + this.activity = activity; + this.serviceId= serviceId; + this.rootView = rootView; + } + + @Override + public void onLoadingStarted(String imageUri, View view) {} + + @Override + public void onLoadingFailed(String imageUri, View view, FailReason failReason) { + ErrorActivity.reportError(activity, + failReason.getCause(), null, rootView, + ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE, + ServiceList.getNameOfService(serviceId), imageUri, + R.string.could_not_load_image)); + } + + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + + } + + @Override + public void onLoadingCancelled(String imageUri, View view) {} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java new file mode 100644 index 000000000..e8f7189a4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -0,0 +1,69 @@ +package org.schabi.newpipe; + +import android.content.Intent; +import android.media.AudioManager; +import android.support.v4.app.Fragment; +import android.support.v4.app.NavUtils; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import org.schabi.newpipe.settings.SettingsActivity; + +/** + * Created by Christian Schabesberger on 02.08.16. + */ + +public class MainActivity extends AppCompatActivity { + + private Fragment mainFragment = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + setVolumeControlStream(AudioManager.STREAM_MUSIC); + mainFragment = getSupportFragmentManager() + .findFragmentById(R.id.search_fragment); + } + + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + + inflater.inflate(R.menu.main_menu, menu); + + mainFragment.onCreateOptionsMenu(menu, inflater); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + switch(id) { + case android.R.id.home: { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + NavUtils.navigateUpTo(this, intent); + return true; + } + case R.id.action_settings: { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + return true; + } + case R.id.action_show_downloads: { + Intent intent = new Intent(this, org.schabi.newpipe.download.MainActivity.class); + startActivity(intent); + return true; + } + default: + return mainFragment.onOptionsItemSelected(item) || + super.onOptionsItemSelected(item); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java b/app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java deleted file mode 100644 index 92b44f807..000000000 --- a/app/src/main/java/org/schabi/newpipe/VideoItemListActivity.java +++ /dev/null @@ -1,411 +0,0 @@ -package org.schabi.newpipe; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.media.AudioManager; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.support.v4.app.NavUtils; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.SearchView; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Toast; - -import org.schabi.newpipe.extractor.ExtractionException; -import org.schabi.newpipe.extractor.SearchEngine; -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; - -import java.io.IOException; -import java.util.List; -import java.util.Vector; - -/** - * Copyright (C) Christian Schabesberger 2015 - * VideoItemListActivity.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 . - */ - -public class VideoItemListActivity extends AppCompatActivity - implements VideoItemListFragment.Callbacks { - - private static final String TAG = VideoItemListFragment.class.toString(); - - // arguments to give to this activity - public static final String VIDEO_INFO_ITEMS = "video_info_items"; - - // savedInstanceBundle arguments - private static final String QUERY = "query"; - private static final String STREAMING_SERVICE = "streaming_service"; - - // activity modes - private static final int SEARCH_MODE = 0; - private static final int PRESENT_VIDEOS_MODE = 1; - - private int mode; - private int currentStreamingServiceId = -1; - private String searchQuery = ""; - - private VideoItemListFragment listFragment; - private VideoItemDetailFragment videoFragment; - private Menu menu; - - private SuggestionListAdapter suggestionListAdapter; - private SuggestionSearchRunnable suggestionSearchRunnable; - private Thread searchThread; - - private class SearchVideoQueryListener implements SearchView.OnQueryTextListener { - - @Override - public boolean onQueryTextSubmit(String query) { - try { - searchQuery = query; - listFragment.search(query); - - // hide virtual keyboard - InputMethodManager inputManager = - (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - try { - //noinspection ConstantConditions - inputManager.hideSoftInputFromWindow( - getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); - } catch(NullPointerException e) { - Log.e(TAG, "Could not get widget with focus"); - Toast.makeText(VideoItemListActivity.this, "Could not get widget with focus", - Toast.LENGTH_SHORT).show(); - e.printStackTrace(); - } - // clear focus - // 1. to not open up the keyboard after switching back to this - // 2. It's a workaround to a seeming bug by the Android OS it self, causing - // onQueryTextSubmit to trigger twice when focus is not cleared. - // See: http://stackoverflow.com/questions/17874951/searchview-onquerytextsubmit-runs-twice-while-i-pressed-once - getCurrentFocus().clearFocus(); - } catch(Exception e) { - e.printStackTrace(); - } - View bg = findViewById(R.id.mainBG); - bg.setVisibility(View.GONE); - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - if(!newText.isEmpty()) { - searchSuggestions(newText); - } - return true; - } - - } - private class SearchSuggestionListener implements SearchView.OnSuggestionListener{ - - private SearchView searchView; - - private SearchSuggestionListener(SearchView searchView) { - this.searchView = searchView; - } - - @Override - public boolean onSuggestionSelect(int position) { - String suggestion = suggestionListAdapter.getSuggestion(position); - searchView.setQuery(suggestion,true); - return false; - } - - @Override - public boolean onSuggestionClick(int position) { - String suggestion = suggestionListAdapter.getSuggestion(position); - searchView.setQuery(suggestion,true); - return false; - } - } - - private class SuggestionResultRunnable implements Runnable{ - - private List suggestions; - - private SuggestionResultRunnable(List suggestions) { - this.suggestions = suggestions; - } - - @Override - public void run() { - suggestionListAdapter.updateAdapter(suggestions); - } - } - - private class SuggestionSearchRunnable implements Runnable{ - private final int serviceId; - private final String query; - final Handler h = new Handler(); - private Context context; - private SuggestionSearchRunnable(int serviceId, String query) { - this.serviceId = serviceId; - this.query = query; - context = VideoItemListActivity.this; - } - - @Override - public void run() { - try { - SearchEngine engine = - ServiceList.getService(serviceId).getSearchEngineInstance(new Downloader()); - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - String searchLanguageKey = context.getString(R.string.search_language_key); - String searchLanguage = sp.getString(searchLanguageKey, - getString(R.string.default_language_value)); - List suggestions = engine.suggestionList(query,searchLanguage,new Downloader()); - h.post(new SuggestionResultRunnable(suggestions)); - } catch (ExtractionException e) { - ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), - ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, - ServiceList.getNameOfService(serviceId), query, R.string.parsing_error)); - e.printStackTrace(); - } catch (IOException e) { - postNewErrorToast(h, R.string.network_error); - e.printStackTrace(); - } catch (Exception e) { - ErrorActivity.reportError(h, VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), - ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, - ServiceList.getNameOfService(serviceId), query, R.string.general_error)); - } - } - } - /** - * Whether or not the activity is in two-pane mode, i.e. running on a tablet - * device. - */ - private boolean mTwoPane; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(R.style.AppTheme); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_videoitem_list); - StreamingService streamingService = null; - - View bg = findViewById(R.id.mainBG); - bg.setVisibility(View.VISIBLE); - - try { - //------ todo: remove this line when multiservice support is implemented ------ - currentStreamingServiceId = ServiceList.getIdOfService("Youtube"); - streamingService = ServiceList.getService(currentStreamingServiceId); - } catch (Exception e) { - e.printStackTrace(); - ErrorActivity.reportError(VideoItemListActivity.this, e, null, findViewById(R.id.videoitem_list), - ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, - ServiceList.getNameOfService(currentStreamingServiceId), "", R.string.general_error)); - } - setVolumeControlStream(AudioManager.STREAM_MUSIC); - listFragment = (VideoItemListFragment) getSupportFragmentManager() - .findFragmentById(R.id.videoitem_list); - listFragment.setStreamingService(streamingService); - - if(savedInstanceState != null - && mode != PRESENT_VIDEOS_MODE) { - searchQuery = savedInstanceState.getString(QUERY); - currentStreamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE); - if(!searchQuery.isEmpty()) { - listFragment.search(searchQuery); - } - } - - if (findViewById(R.id.videoitem_detail_container) != null) { - // The detail container view will be present only in the - // large-screen layouts (res/values-large and - // res/values-sw600dp). If this view is present, then the - // activity should be in two-pane mode. - mTwoPane = true; - - // In two-pane mode, list items should be given the - // 'activated' state when touched. - - ((VideoItemListFragment) getSupportFragmentManager() - .findFragmentById(R.id.videoitem_list)) - .setActivateOnItemClick(true); - - SearchView searchView = (SearchView)findViewById(R.id.searchViewTablet); - if(mode != PRESENT_VIDEOS_MODE) { - // Somehow the seticonifiedbydefault property set by the layout xml is not working on - // the support version on SearchView, so it needs to be set programmatically. - searchView.setIconifiedByDefault(false); - searchView.setIconified(false); - if(!searchQuery.isEmpty()) { - searchView.setQuery(searchQuery,false); - } - searchView.setOnQueryTextListener(new SearchVideoQueryListener()); - suggestionListAdapter = new SuggestionListAdapter(this); - searchView.setSuggestionsAdapter(suggestionListAdapter); - searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView)); - } else { - searchView.setVisibility(View.GONE); - } - } - - PreferenceManager.setDefaultValues(this, R.xml.settings, false); - } - - @Override - public void onResume() { - super.onResume(); - App.checkStartTor(this); - } - - /** - * Callback method from {@link VideoItemListFragment.Callbacks} - * indicating that the item with the given ID was selected. - */ - @Override - public void onItemSelected(String id) { - VideoListAdapter listAdapter = (VideoListAdapter) ((VideoItemListFragment) - getSupportFragmentManager() - .findFragmentById(R.id.videoitem_list)) - .getListAdapter(); - String webpageUrl = listAdapter.getVideoList().get((int) Long.parseLong(id)).webpage_url; - if (mTwoPane) { - // In two-pane mode, show the detail view in this activity by - // adding or replacing the detail fragment using a - // fragment transaction. - Bundle arguments = new Bundle(); - //arguments.putString(VideoItemDetailFragment.ARG_ITEM_ID, id); - arguments.putString(VideoItemDetailFragment.VIDEO_URL, webpageUrl); - arguments.putInt(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId); - videoFragment = new VideoItemDetailFragment(); - videoFragment.setArguments(arguments); - videoFragment.setOnInvokeCreateOptionsMenuListener(new VideoItemDetailFragment.OnInvokeCreateOptionsMenuListener() { - @Override - public void createOptionsMenu() { - menu.clear(); - onCreateOptionsMenu(menu); - } - }); - getSupportFragmentManager().beginTransaction() - .replace(R.id.videoitem_detail_container, videoFragment) - .commit(); - } else { - // In single-pane mode, simply start the detail activity - // for the selected item ID. - Intent detailIntent = new Intent(this, VideoItemDetailActivity.class); - //detailIntent.putExtra(VideoItemDetailFragment.ARG_ITEM_ID, id); - detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, webpageUrl); - detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, currentStreamingServiceId); - startActivity(detailIntent); - } - } - - - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - this.menu = menu; - MenuInflater inflater = getMenuInflater(); - if(mode != PRESENT_VIDEOS_MODE && - findViewById(R.id.videoitem_detail_container) == null) { - inflater.inflate(R.menu.videoitem_list, menu); - MenuItem searchItem = menu.findItem(R.id.action_search); - SearchView searchView = (SearchView) searchItem.getActionView(); - searchView.setFocusable(false); - searchView.setOnQueryTextListener( - new SearchVideoQueryListener()); - suggestionListAdapter = new SuggestionListAdapter(this); - searchView.setSuggestionsAdapter(suggestionListAdapter); - searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView)); - if(!searchQuery.isEmpty()) { - searchView.setQuery(searchQuery,false); - searchView.setIconifiedByDefault(false); - } - } else if (videoFragment != null){ - videoFragment.onCreateOptionsMenu(menu, inflater); - } else { - inflater.inflate(R.menu.videoitem_two_pannel, menu); - } - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - switch(id) { - case android.R.id.home: { - Intent intent = new Intent(this, VideoItemListActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - NavUtils.navigateUpTo(this, intent); - return true; - } - case R.id.action_settings: { - Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - return true; - } - case R.id.action_report_error: { - ErrorActivity.reportError(VideoItemListActivity.this, new Vector(), - null, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.USER_REPORT, - ServiceList.getNameOfService(currentStreamingServiceId), - "user_report", R.string.user_report)); - return true; - } - case R.id.action_show_downloads: { - Intent intent = new Intent(this, org.schabi.newpipe.download.MainActivity.class); - startActivity(intent); - return true; - } - default: - return videoFragment.onOptionsItemSelected(item) || - super.onOptionsItemSelected(item); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - /* - if (mActivatedPosition != ListView.INVALID_POSITION) { - // Serialize and persist the activated item position. - outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition); - } - */ - outState.putString(QUERY, searchQuery); - outState.putInt(STREAMING_SERVICE, currentStreamingServiceId); - } - - private void searchSuggestions(String query) { - suggestionSearchRunnable = - new SuggestionSearchRunnable(currentStreamingServiceId, query); - searchThread = new Thread(suggestionSearchRunnable); - searchThread.start(); - - } - - private void postNewErrorToast(Handler h, final int stringResource) { - h.post(new Runnable() { - @Override - public void run() { - Toast.makeText(VideoItemListActivity.this, getString(stringResource), - Toast.LENGTH_SHORT).show(); - } - }); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemListFragment.java b/app/src/main/java/org/schabi/newpipe/VideoItemListFragment.java deleted file mode 100644 index 7c7d0588c..000000000 --- a/app/src/main/java/org/schabi/newpipe/VideoItemListFragment.java +++ /dev/null @@ -1,370 +0,0 @@ -package org.schabi.newpipe; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.support.v4.app.ListFragment; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AbsListView; -import android.widget.ListView; -import android.widget.Toast; - -import java.io.IOException; -import java.util.List; - -import org.schabi.newpipe.extractor.ExtractionException; -import org.schabi.newpipe.extractor.SearchResult; -import org.schabi.newpipe.extractor.StreamPreviewInfo; -import org.schabi.newpipe.extractor.SearchEngine; -import org.schabi.newpipe.extractor.StreamingService; - - -/** - * Copyright (C) Christian Schabesberger 2015 - * VideoItemListFragment.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 . - */ - -public class VideoItemListFragment extends ListFragment { - - private static final String TAG = VideoItemListFragment.class.toString(); - - private StreamingService streamingService; - private VideoListAdapter videoListAdapter; - - // activity modes - private static final int SEARCH_MODE = 0; - private static final int PRESENT_VIDEOS_MODE = 1; - - private int mode; - private String query = ""; - private int lastPage; - - private Thread searchThread; - private SearchRunnable searchRunnable; - // used to track down if results posted by threads ar still valid - private int currentRequestId = -1; - private ListView list; - - private View footer; - - // used to suppress request for loading a new page while another page is already loading. - private boolean loadingNextPage = true; - - private class ResultRunnable implements Runnable { - private final SearchResult result; - private final int requestId; - public ResultRunnable(SearchResult result, int requestId) { - this.result = result; - this.requestId = requestId; - } - @Override - public void run() { - updateListOnResult(result, requestId); - if (android.os.Build.VERSION.SDK_INT >= 19) { - getListView().removeFooterView(footer); - } - } - } - - private class SearchRunnable implements Runnable { - public static final String YOUTUBE = "Youtube"; - private final SearchEngine engine; - private final String query; - private final int page; - final Handler h = new Handler(); - private volatile boolean runs = true; - private final int requestId; - public SearchRunnable(SearchEngine engine, String query, int page, int requestId) { - this.engine = engine; - this.query = query; - this.page = page; - this.requestId = requestId; - } - void terminate() { - runs = false; - } - @Override - public void run() { - SearchResult result = null; - try { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); - String searchLanguageKey = getContext().getString(R.string.search_language_key); - String searchLanguage = sp.getString(searchLanguageKey, - getString(R.string.default_language_value)); - result = SearchResult - .getSearchResult(engine, query, page, searchLanguage, new Downloader()); - - if(runs) { - h.post(new ResultRunnable(result, requestId)); - } - - // look for errors during extraction - // soft errors: - if(result != null && - !result.errors.isEmpty()) { - Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:"); - for(Exception e : result.errors) { - e.printStackTrace(); - Log.e(TAG, "------"); - } - - Activity a = getActivity(); - View rootView = a.findViewById(R.id.videoitem_list); - ErrorActivity.reportError(h, getActivity(), result.errors, null, rootView, - ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, - /* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.light_parsing_error)); - - } - // hard errors: - } catch(IOException e) { - postNewNothingFoundToast(h, R.string.network_error); - e.printStackTrace(); - } catch(SearchEngine.NothingFoundException e) { - postNewErrorToast(h, e.getMessage()); - } catch(ExtractionException e) { - ErrorActivity.reportError(h, getActivity(), e, null, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, - /* todo: this shoudl not be assigned static */ - YOUTUBE, query, R.string.parsing_error)); - //postNewErrorToast(h, R.string.parsing_error); - e.printStackTrace(); - - } catch(Exception e) { - ErrorActivity.reportError(h, getActivity(), e, null, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, - /* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.general_error)); - - e.printStackTrace(); - } - } - } - - public void present(List videoList) { - mode = PRESENT_VIDEOS_MODE; - setListShown(true); - getListView().smoothScrollToPosition(0); - - updateList(videoList); - } - - public void search(String query) { - mode = SEARCH_MODE; - this.query = query; - this.lastPage = 1; - videoListAdapter.clearVideoList(); - setListShown(false); - startSearch(query, lastPage); - //todo: Somehow this command is not working on older devices, - // although it was introduced with API level 8. Test this and find a solution. - getListView().smoothScrollToPosition(0); - } - - private void nextPage() { - loadingNextPage = true; - lastPage++; - Log.d(TAG, getString(R.string.search_page) + Integer.toString(lastPage)); - startSearch(query, lastPage); - } - - private void startSearch(String query, int page) { - currentRequestId++; - terminateThreads(); - searchRunnable = new SearchRunnable(streamingService.getSearchEngineInstance(new Downloader()), - query, page, currentRequestId); - searchThread = new Thread(searchRunnable); - searchThread.start(); - } - - public void setStreamingService(StreamingService streamingService) { - this.streamingService = streamingService; - } - - private void updateListOnResult(SearchResult result, int requestId) { - if(requestId == currentRequestId) { - setListShown(true); - updateList(result.resultList); - if(!result.suggestion.isEmpty()) { - Toast.makeText(getActivity(), - String.format(getString(R.string.did_you_mean), result.suggestion), - Toast.LENGTH_LONG).show(); - } - } - } - - private void updateList(List list) { - try { - videoListAdapter.addVideoList(list); - terminateThreads(); - } catch(java.lang.IllegalStateException e) { - Toast.makeText(getActivity(), "Trying to set value while activity doesn't exist anymore.", - Toast.LENGTH_SHORT).show(); - Log.w(TAG, "Trying to set value while activity doesn't exist anymore."); - } catch(Exception e) { - Toast.makeText(getActivity(), getString(R.string.general_error), - Toast.LENGTH_SHORT).show(); - e.printStackTrace(); - } finally { - loadingNextPage = false; - } - } - - private void terminateThreads() { - if(searchThread != null) { - searchRunnable.terminate(); - // No need to join, since we don't really terminate the thread. We just demand - // it to post its result runnable into the gui main loop. - } - } - - /** - * The serialization (saved instance state) Bundle key representing the - * activated item position. Only used on tablets. - */ - private static final String STATE_ACTIVATED_POSITION = "activated_position"; - - /** - * The current activated item position. Only used on tablets. - */ - private int mActivatedPosition = ListView.INVALID_POSITION; - - /** - * A callback interface that all activities containing this fragment must - * implement. This mechanism allows activities to be notified of item - * selections. - */ - public interface Callbacks { - /** - * Callback for when an item has been selected. - */ - void onItemSelected(String id); - } - - private Callbacks mCallbacks; - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - list = getListView(); - videoListAdapter = new VideoListAdapter(getActivity(), this); - footer = ((LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE)) - .inflate(R.layout.paginate_footer, null, false); - - - setListAdapter(videoListAdapter); - - // Restore the previously serialized activated item position. - if (savedInstanceState != null - - && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { - setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); - } - - - getListView().setOnScrollListener(new AbsListView.OnScrollListener() { - long lastScrollDate; - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - if (mode != PRESENT_VIDEOS_MODE - && list.getChildAt(0) != null - && list.getLastVisiblePosition() == list.getAdapter().getCount() - 1 - && list.getChildAt(list.getChildCount() - 1).getBottom() >= list.getHeight()) { - long time = System.currentTimeMillis(); - if ((time - lastScrollDate) > 200 - && !loadingNextPage) { - lastScrollDate = time; - getListView().addFooterView(footer); - nextPage(); - } - } - } - - }); - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - - // Activities containing this fragment must implement its callbacks. - if (!(context instanceof Callbacks)) { - throw new IllegalStateException("Activity must implement fragment's callbacks."); - } - - mCallbacks = (Callbacks) context; - } - - @Override - public void onListItemClick(ListView listView, View view, int position, long id) { - super.onListItemClick(listView, view, position, id); - setActivatedPosition(position); - mCallbacks.onItemSelected(Long.toString(id)); - } - - /** - * Turns on activate-on-click mode. When this mode is on, list items will be - * given the 'activated' state when touched. - */ - public void setActivateOnItemClick(@SuppressWarnings("SameParameterValue") boolean activateOnItemClick) { - // When setting CHOICE_MODE_SINGLE, ListView will automatically - // give items the 'activated' state when touched. - getListView().setChoiceMode(activateOnItemClick - ? ListView.CHOICE_MODE_SINGLE - : ListView.CHOICE_MODE_NONE); - } - - private void setActivatedPosition(int position) { - if (position == ListView.INVALID_POSITION) { - getListView().setItemChecked(mActivatedPosition, false); - } else { - getListView().setItemChecked(position, true); - } - - mActivatedPosition = position; - } - - private void postNewErrorToast(Handler h, final String message) { - h.post(new Runnable() { - @Override - public void run() { - setListShown(true); - Toast.makeText(getActivity(), message, - Toast.LENGTH_SHORT).show(); - } - }); - } - - private void postNewNothingFoundToast(Handler h, final int stringResource) { - h.post(new Runnable() { - @Override - public void run() { - setListShown(true); - Toast.makeText(getActivity(), getString(stringResource), - Toast.LENGTH_LONG).show(); - } - }); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/VideoListAdapter.java b/app/src/main/java/org/schabi/newpipe/VideoListAdapter.java deleted file mode 100644 index d69b842cf..000000000 --- a/app/src/main/java/org/schabi/newpipe/VideoListAdapter.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.schabi.newpipe; - -import android.content.Context; -import android.support.v4.content.ContextCompat; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ListView; - -import org.schabi.newpipe.extractor.StreamPreviewInfo; - -import java.util.List; -import java.util.Vector; - -/** - * Created by Christian Schabesberger on 11.08.15. - * - * Copyright (C) Christian Schabesberger 2015 - * VideoListAdapter.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 . - */ - -class VideoListAdapter extends BaseAdapter { - private final Context context; - private final VideoInfoItemViewCreator viewCreator; - private Vector videoList = new Vector<>(); - private final ListView listView; - - public VideoListAdapter(Context context, VideoItemListFragment videoListFragment) { - viewCreator = new VideoInfoItemViewCreator(LayoutInflater.from(context)); - this.listView = videoListFragment.getListView(); - this.listView.setDivider(null); - this.listView.setDividerHeight(0); - this.context = context; - } - - public void addVideoList(List videos) { - videoList.addAll(videos); - notifyDataSetChanged(); - } - - public void clearVideoList() { - videoList = new Vector<>(); - notifyDataSetChanged(); - } - - public Vector getVideoList() { - return videoList; - } - - @Override - public int getCount() { - return videoList.size(); - } - - @Override - public Object getItem(int position) { - return videoList.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - convertView = viewCreator.getViewFromVideoInfoItem(convertView, parent, videoList.get(position)); - - if(listView.isItemChecked(position)) { - convertView.setBackgroundColor(ContextCompat.getColor(context,R.color.light_youtube_primary_color)); - } else { - convertView.setBackgroundColor(0); - } - - return convertView; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/ActionBarHandler.java rename to app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java index 80c149885..feed040d1 100644 --- a/app/src/main/java/org/schabi/newpipe/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.detail; import android.content.Intent; import android.content.SharedPreferences; @@ -11,8 +11,9 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.widget.ArrayAdapter; +import org.schabi.newpipe.R; +import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.StreamInfo; import org.schabi.newpipe.extractor.VideoStream; import java.util.List; diff --git a/app/src/main/java/org/schabi/newpipe/detail/StreamInfoWorker.java b/app/src/main/java/org/schabi/newpipe/detail/StreamInfoWorker.java new file mode 100644 index 000000000..03178f9d1 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/detail/StreamInfoWorker.java @@ -0,0 +1,204 @@ +package org.schabi.newpipe.detail; + +import android.app.Activity; +import android.os.Handler; +import android.util.Log; +import android.view.View; + +import org.schabi.newpipe.Downloader; +import org.schabi.newpipe.ErrorActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.StreamExtractor; +import org.schabi.newpipe.extractor.StreamInfo; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; + +import java.io.IOException; + +/** + * Created by the-scrabi on 02.08.16. + */ + +public class StreamInfoWorker { + + private static final String TAG = StreamInfoWorker.class.toString(); + + public interface OnStreamInfoReceivedListener { + void onReceive(StreamInfo info); + void onError(int messageId); + void onBlockedByGemaError(); + void onContentErrorWithMessage(int messageId); + void onContentError(); + } + + private class StreamExtractorRunnable implements Runnable { + private final Handler h = new Handler(); + private StreamExtractor streamExtractor; + private final int serviceId; + private final String videoUrl; + private Activity a; + + public StreamExtractorRunnable(Activity a, String videoUrl, int serviceId) { + this.serviceId = serviceId; + this.videoUrl = videoUrl; + this.a = a; + } + + @Override + public void run() { + StreamInfo streamInfo = null; + StreamingService service = null; + try { + service = ServiceList.getService(serviceId); + } catch (Exception e) { + e.printStackTrace(); + ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + "", videoUrl, R.string.could_not_get_stream)); + return; + } + try { + streamExtractor = service.getExtractorInstance(videoUrl, new Downloader()); + streamInfo = StreamInfo.getVideoInfo(streamExtractor, new Downloader()); + + final StreamInfo info = streamInfo; + h.post(new Runnable() { + @Override + public void run() { + onStreamInfoReceivedListener.onReceive(info); + } + }); + + // look for errors during extraction + // this if statement only covers extra information. + // if these are not available or caused an error, they are just not available + // but don't render the stream information unusalbe. + if(streamInfo != null && + !streamInfo.errors.isEmpty()) { + Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:"); + for (Throwable e : streamInfo.errors) { + e.printStackTrace(); + Log.e(TAG, "------"); + } + + View rootView = a != null ? a.findViewById(R.id.video_item_detail) : null; + ErrorActivity.reportError(h, a, + streamInfo.errors, null, rootView, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, 0 /* no message for the user */)); + } + + // These errors render the stream information unusable. + } catch (IOException e) { + h.post(new Runnable() { + @Override + public void run() { + onStreamInfoReceivedListener.onError(R.string.network_error); + } + }); + e.printStackTrace(); + } + // custom service related exceptions + catch (YoutubeStreamExtractor.DecryptException de) { + h.post(new Runnable() { + @Override + public void run() { + onStreamInfoReceivedListener.onError(R.string.youtube_signature_decryption_error); + } + }); + de.printStackTrace(); + } catch (YoutubeStreamExtractor.GemaException ge) { + h.post(new Runnable() { + @Override + public void run() { + onStreamInfoReceivedListener.onBlockedByGemaError(); + } + }); + } catch(YoutubeStreamExtractor.LiveStreamException e) { + h.post(new Runnable() { + @Override + public void run() { + onStreamInfoReceivedListener + .onContentErrorWithMessage(R.string.live_streams_not_supported); + } + }); + } + // ---------------------------------------- + catch(StreamExtractor.ContentNotAvailableException e) { + h.post(new Runnable() { + @Override + public void run() { + onStreamInfoReceivedListener + .onContentError(); + } + }); + e.printStackTrace(); + } catch(StreamInfo.StreamExctractException e) { + if(!streamInfo.errors.isEmpty()) { + // !!! if this case ever kicks in someone gets kicked out !!! + ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); + } else { + ErrorActivity.reportError(h, a, streamInfo.errors, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); + } + h.post(new Runnable() { + @Override + public void run() { + a.finish(); + } + }); + e.printStackTrace(); + } catch (ParsingException e) { + ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.parsing_error)); + h.post(new Runnable() { + @Override + public void run() { + a.finish(); + } + }); + e.printStackTrace(); + } catch(Exception e) { + ErrorActivity.reportError(h, a, e, VideoItemDetailFragment.class, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, + service.getServiceInfo().name, videoUrl, R.string.general_error)); + h.post(new Runnable() { + @Override + public void run() { + a.finish(); + } + }); + e.printStackTrace(); + } + } + } + + private static StreamInfoWorker streamInfoWorker = null; + private StreamExtractorRunnable runnable = null; + private OnStreamInfoReceivedListener onStreamInfoReceivedListener = null; + + private StreamInfoWorker() { + + } + + public static StreamInfoWorker getInstance() { + return streamInfoWorker == null ? (streamInfoWorker = new StreamInfoWorker()) : streamInfoWorker; + } + + public void search(int serviceId, String url, Activity a) { + runnable = new StreamExtractorRunnable(a, url, serviceId); + Thread thread = new Thread(runnable); + thread.start(); + } + + public void setOnStreamInfoReceivedListener( + OnStreamInfoReceivedListener onStreamInfoReceivedListener) { + this.onStreamInfoReceivedListener = onStreamInfoReceivedListener; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemDetailActivity.java b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailActivity.java similarity index 95% rename from app/src/main/java/org/schabi/newpipe/VideoItemDetailActivity.java rename to app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailActivity.java index 9ba18bdcc..04649bf4d 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoItemDetailActivity.java +++ b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailActivity.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.detail; import android.content.Intent; import android.media.AudioManager; @@ -11,6 +11,9 @@ import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; +import org.schabi.newpipe.App; +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.StreamingService; @@ -85,7 +88,7 @@ public class VideoItemDetailActivity extends AppCompatActivity { .show(); } //arguments.putString(VideoItemDetailFragment.VIDEO_URL, - // videoExtractor.getVideoUrl(videoExtractor.getVideoId(videoUrl)));//cleans URL + // videoExtractor.getUrl(videoExtractor.getId(videoUrl)));//cleans URL arguments.putString(VideoItemDetailFragment.VIDEO_URL, videoUrl); arguments.putBoolean(VideoItemDetailFragment.AUTO_PLAY, @@ -138,7 +141,7 @@ public class VideoItemDetailActivity extends AppCompatActivity { // http://developer.android.com/design/patterns/navigation.html#up-vs-back - Intent intent = new Intent(this, VideoItemListActivity.class); + Intent intent = new Intent(this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); NavUtils.navigateUpTo(this, intent); return true; diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java similarity index 66% rename from app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java rename to app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java index c9f277562..2c5f2f46e 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java @@ -1,22 +1,21 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.detail; import android.app.Activity; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Point; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.preference.PreferenceManager; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.Fragment; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.text.Html; import android.text.method.LinkMovementMethod; import android.util.Log; @@ -27,38 +26,34 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import android.view.MenuItem; import android.widget.Toast; -import java.io.IOException; - import com.google.android.exoplayer.util.Util; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; -import java.util.ArrayList; import java.util.Vector; - +import org.schabi.newpipe.ActivityCommunicator; +import org.schabi.newpipe.ChannelActivity; +import org.schabi.newpipe.ErrorActivity; +import org.schabi.newpipe.ImageErrorLoadingListener; +import org.schabi.newpipe.Localization; +import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.AudioStream; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.ParsingException; import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamExtractor; import org.schabi.newpipe.extractor.StreamInfo; -import org.schabi.newpipe.extractor.StreamPreviewInfo; -import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.VideoStream; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.PlayVideoActivity; import org.schabi.newpipe.player.ExoPlayerActivity; @@ -102,19 +97,20 @@ public class VideoItemDetailFragment extends Fragment { private int streamingServiceId = -1; private boolean autoPlayEnabled; - private boolean showNextVideoItem; - private Bitmap videoThumbnail; + private boolean showNextStreamItem; private View thumbnailWindowLayout; //this only remains due to downwards compatibility private FloatingActionButton playVideoButton; private final Point initialThumbnailPos = new Point(0, 0); - + private View rootView = null; + private Bitmap streamThumbnail = null; private ImageLoader imageLoader = ImageLoader.getInstance(); private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); + private InfoListAdapter similarStreamsAdapter = null; public interface OnInvokeCreateOptionsMenuListener { void createOptionsMenu(); @@ -122,215 +118,57 @@ public class VideoItemDetailFragment extends Fragment { private OnInvokeCreateOptionsMenuListener onInvokeCreateOptionsMenuListener; - private class VideoExtractorRunnable implements Runnable { - private final Handler h = new Handler(); - private StreamExtractor streamExtractor; - private final StreamingService service; - private final String videoUrl; - - public VideoExtractorRunnable(String videoUrl, StreamingService service) { - this.service = service; - this.videoUrl = videoUrl; - } - - @Override - public void run() { - StreamInfo streamInfo = null; - try { - streamExtractor = service.getExtractorInstance(videoUrl, new Downloader()); - streamInfo = StreamInfo.getVideoInfo(streamExtractor, new Downloader()); - - h.post(new VideoResultReturnedRunnable(streamInfo)); - - // look for errors during extraction - // this if statement only covers extra information. - // if these are not available or caused an error, they are just not available - // but don't render the stream information unusalbe. - if(streamInfo != null && - !streamInfo.errors.isEmpty()) { - Log.e(TAG, "OCCURRED ERRORS DURING EXTRACTION:"); - for (Exception e : streamInfo.errors) { - e.printStackTrace(); - Log.e(TAG, "------"); - } - - Activity a = getActivity(); - View rootView = a != null ? a.findViewById(R.id.videoitem_detail) : null; - ErrorActivity.reportError(h, getActivity(), - streamInfo.errors, null, rootView, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, 0 /* no message for the user */)); - } - - // These errors render the stream information unusable. - } catch (IOException e) { - postNewErrorToast(h, R.string.network_error); - e.printStackTrace(); - } - // custom service related exceptions - catch (YoutubeStreamExtractor.DecryptException de) { - postNewErrorToast(h, R.string.youtube_signature_decryption_error); - de.printStackTrace(); - } catch (YoutubeStreamExtractor.GemaException ge) { - h.post(new Runnable() { - @Override - public void run() { - onErrorBlockedByGema(); - } - }); - } catch(YoutubeStreamExtractor.LiveStreamException e) { - h.post(new Runnable() { - @Override - public void run() { - onNotSpecifiedContentErrorWithMessage(R.string.live_streams_not_supported); - } - }); - } - // ---------------------------------------- - catch(StreamExtractor.ContentNotAvailableException e) { - h.post(new Runnable() { - @Override - public void run() { - onNotSpecifiedContentError(); - } - }); - e.printStackTrace(); - } catch(StreamInfo.StreamExctractException e) { - if(!streamInfo.errors.isEmpty()) { - // !!! if this case ever kicks in someone gets kicked out !!! - ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); - } else { - ErrorActivity.reportError(h, getActivity(), streamInfo.errors, VideoItemListActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); - } - h.post(new Runnable() { - @Override - public void run() { - getActivity().finish(); - } - }); - e.printStackTrace(); - } catch (ParsingException e) { - ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.parsing_error)); - h.post(new Runnable() { - @Override - public void run() { - getActivity().finish(); - } - }); - e.printStackTrace(); - } catch(Exception e) { - ErrorActivity.reportError(h, getActivity(), e, VideoItemListActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.general_error)); - h.post(new Runnable() { - @Override - public void run() { - getActivity().finish(); - } - }); - e.printStackTrace(); - } - } - } - - private class VideoResultReturnedRunnable implements Runnable { - private final StreamInfo streamInfo; - public VideoResultReturnedRunnable(StreamInfo streamInfo) { - this.streamInfo = streamInfo; - } - @Override - public void run() { - Activity a = getActivity(); - if(a != null) { - boolean showAgeRestrictedContent = PreferenceManager.getDefaultSharedPreferences(a) - .getBoolean(activity.getString(R.string.show_age_restricted_content), false); - if (streamInfo.age_limit == 0 || showAgeRestrictedContent) { - updateInfo(streamInfo); - } else { - onNotSpecifiedContentErrorWithMessage(R.string.video_is_age_restricted); - } - } - } - } - - private class ThumbnailLoadingListener implements ImageLoadingListener { - @Override - public void onLoadingStarted(String imageUri, View view) {} - - @Override - public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - if(getContext() != null) { - Toast.makeText(VideoItemDetailFragment.this.getActivity(), - R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show(); - } - failReason.getCause().printStackTrace(); - } - - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {} - - @Override - public void onLoadingCancelled(String imageUri, View view) {} - } - private void updateInfo(final StreamInfo info) { try { - Context c = getContext(); - VideoInfoItemViewCreator videoItemViewCreator = - new VideoInfoItemViewCreator(LayoutInflater.from(getActivity())); + Activity a = getActivity(); RelativeLayout textContentLayout = - (RelativeLayout) activity.findViewById(R.id.detailTextContentLayout); + (RelativeLayout) activity.findViewById(R.id.detail_text_content_layout); final TextView videoTitleView = - (TextView) activity.findViewById(R.id.detailVideoTitleView); - TextView uploaderView = (TextView) activity.findViewById(R.id.detailUploaderView); - TextView viewCountView = (TextView) activity.findViewById(R.id.detailViewCountView); - TextView thumbsUpView = (TextView) activity.findViewById(R.id.detailThumbsUpCountView); + (TextView) activity.findViewById(R.id.detail_video_title_view); + TextView uploaderView = (TextView) activity.findViewById(R.id.detail_uploader_view); + TextView viewCountView = (TextView) activity.findViewById(R.id.detail_view_count_view); + TextView thumbsUpView = (TextView) activity.findViewById(R.id.detail_thumbs_up_count_view); TextView thumbsDownView = - (TextView) activity.findViewById(R.id.detailThumbsDownCountView); - TextView uploadDateView = (TextView) activity.findViewById(R.id.detailUploadDateView); - TextView descriptionView = (TextView) activity.findViewById(R.id.detailDescriptionView); - FrameLayout nextVideoFrame = - (FrameLayout) activity.findViewById(R.id.detailNextVideoFrame); + (TextView) activity.findViewById(R.id.detail_thumbs_down_count_view); + TextView uploadDateView = (TextView) activity.findViewById(R.id.detail_upload_date_view); + TextView descriptionView = (TextView) activity.findViewById(R.id.detail_description_view); + RecyclerView nextStreamView = + (RecyclerView) activity.findViewById(R.id.detail_next_stream_content); RelativeLayout nextVideoRootFrame = - (RelativeLayout) activity.findViewById(R.id.detailNextVideoRootLayout); - Button nextVideoButton = (Button) activity.findViewById(R.id.detailNextVideoButton); - TextView similarTitle = (TextView) activity.findViewById(R.id.detailSimilarTitle); + (RelativeLayout) activity.findViewById(R.id.detail_next_stream_root_layout); + TextView similarTitle = (TextView) activity.findViewById(R.id.detail_similar_title); Button backgroundButton = (Button) - activity.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton); + activity.findViewById(R.id.detail_stream_thumbnail_window_background_button); View topView = activity.findViewById(R.id.detailTopView); - View nextVideoView = null; - if(info.next_video != null) { - nextVideoView = videoItemViewCreator - .getViewFromVideoInfoItem(null, nextVideoFrame, info.next_video); - } else { - activity.findViewById(R.id.detailNextVidButtonAndContentLayout).setVisibility(View.GONE); - activity.findViewById(R.id.detailNextVideoTitle).setVisibility(View.GONE); - activity.findViewById(R.id.detailNextVideoButton).setVisibility(View.GONE); - } + Button channelButton = (Button) activity.findViewById(R.id.channel_button); progressBar.setVisibility(View.GONE); - if(nextVideoView != null) { - nextVideoFrame.addView(nextVideoView); + if(info.next_video != null) { + InfoListAdapter adapter = new InfoListAdapter(a, rootView); + nextStreamView.setAdapter(adapter); + nextStreamView.setLayoutManager(new LinearLayoutManager(a)); + adapter.setOnItemSelectedListener(new InfoListAdapter.OnItemSelectedListener() { + @Override + public void selected(String url) { + openStreamUrl(url); + } + }); + } else { + nextStreamView.setVisibility(View.GONE); + activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE); } - initThumbnailViews(info, nextVideoFrame); textContentLayout.setVisibility(View.VISIBLE); if (android.os.Build.VERSION.SDK_INT < 18) { playVideoButton.setVisibility(View.VISIBLE); } else { - ImageView playArrowView = (ImageView) activity.findViewById(R.id.playArrowView); + ImageView playArrowView = (ImageView) activity.findViewById(R.id.play_arrow_view); playArrowView.setVisibility(View.VISIBLE); } - if (!showNextVideoItem) { + if (!showNextStreamItem) { nextVideoRootFrame.setVisibility(View.GONE); similarTitle.setVisibility(View.GONE); } @@ -341,7 +179,7 @@ public class VideoItemDetailFragment extends Fragment { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == android.view.MotionEvent.ACTION_UP) { - ImageView arrow = (ImageView) activity.findViewById(R.id.toggleDescriptionView); + ImageView arrow = (ImageView) activity.findViewById(R.id.toggle_description_view); View extra = activity.findViewById(R.id.detailExtraView); if (extra.getVisibility() == View.VISIBLE) { extra.setVisibility(View.GONE); @@ -361,29 +199,29 @@ public class VideoItemDetailFragment extends Fragment { if(!info.uploader.isEmpty()) { uploaderView.setText(info.uploader); } else { - activity.findViewById(R.id.detailUploaderWrapView).setVisibility(View.GONE); + activity.findViewById(R.id.detail_uploader_view).setVisibility(View.GONE); } if(info.view_count >= 0) { - viewCountView.setText(Localization.localizeViewCount(info.view_count, c)); + viewCountView.setText(Localization.localizeViewCount(info.view_count, a)); } else { viewCountView.setVisibility(View.GONE); } if(info.dislike_count >= 0) { - thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, c)); + thumbsDownView.setText(Localization.localizeNumber(info.dislike_count, a)); } else { thumbsDownView.setVisibility(View.INVISIBLE); - activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE); + activity.findViewById(R.id.detail_thumbs_down_count_view).setVisibility(View.GONE); } if(info.like_count >= 0) { - thumbsUpView.setText(Localization.localizeNumber(info.like_count, c)); + thumbsUpView.setText(Localization.localizeNumber(info.like_count, a)); } else { thumbsUpView.setVisibility(View.GONE); - activity.findViewById(R.id.detailThumbsUpImgView).setVisibility(View.GONE); + activity.findViewById(R.id.detail_thumbs_up_img_view).setVisibility(View.GONE); thumbsDownView.setVisibility(View.GONE); - activity.findViewById(R.id.detailThumbsDownImgView).setVisibility(View.GONE); + activity.findViewById(R.id.detail_thumbs_down_img_view).setVisibility(View.GONE); } if(!info.upload_date.isEmpty()) { - uploadDateView.setText(Localization.localizeDate(info.upload_date, c)); + uploadDateView.setText(Localization.localizeDate(info.upload_date, a)); } else { uploadDateView.setVisibility(View.GONE); } @@ -403,26 +241,17 @@ public class VideoItemDetailFragment extends Fragment { } } - nextVideoButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent detailIntent = - new Intent(getActivity(), VideoItemDetailActivity.class); - /*detailIntent.putExtra( - VideoItemDetailFragment.ARG_ITEM_ID, currentVideoInfo.nextVideo.id); */ - detailIntent.putExtra( - VideoItemDetailFragment.VIDEO_URL, info.next_video.webpage_url); - detailIntent.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId); - startActivity(detailIntent); - } - }); textContentLayout.setVisibility(View.VISIBLE); - if(info.related_videos != null && !info.related_videos.isEmpty()) { - initSimilarVideos(info, videoItemViewCreator); + if(info.next_video == null) { + activity.findViewById(R.id.detail_next_stream_title).setVisibility(View.GONE); + } + + if(info.related_streams != null && !info.related_streams.isEmpty()) { + initSimilarVideos(info); } else { - activity.findViewById(R.id.detailSimilarTitle).setVisibility(View.GONE); - activity.findViewById(R.id.similarVideosView).setVisibility(View.GONE); + activity.findViewById(R.id.detail_similar_title).setVisibility(View.GONE); + activity.findViewById(R.id.similar_streams_view).setVisibility(View.GONE); } setupActionBarHandler(info); @@ -447,18 +276,32 @@ public class VideoItemDetailFragment extends Fragment { } }); + if(info.channel_url != null && info.channel_url != "") { + channelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent i = new Intent(activity, ChannelActivity.class); + i.putExtra(ChannelActivity.CHANNEL_URL, info.channel_url); + i.putExtra(ChannelActivity.SERVICE_ID, info.service_id); + startActivity(i); + } + }); + } else { + channelButton.setVisibility(Button.GONE); + } + + initThumbnailViews(info); + } catch (java.lang.NullPointerException e) { Log.w(TAG, "updateInfo(): Fragment closed before thread ended work... or else"); e.printStackTrace(); } } - private void initThumbnailViews(StreamInfo info, View nextVideoFrame) { - ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); + private void initThumbnailViews(final StreamInfo info) { + ImageView videoThumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view); ImageView uploaderThumb - = (ImageView) activity.findViewById(R.id.detailUploaderThumbnailView); - ImageView nextVideoThumb = - (ImageView) nextVideoFrame.findViewById(R.id.itemThumbnailView); + = (ImageView) activity.findViewById(R.id.detail_uploader_thumbnail_view); if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) { imageLoader.displayImage(info.thumbnail_url, videoThumbnailView, @@ -469,14 +312,16 @@ public class VideoItemDetailFragment extends Fragment { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - Toast.makeText(VideoItemDetailFragment.this.getActivity(), - R.string.could_not_load_thumbnails, Toast.LENGTH_LONG).show(); - failReason.getCause().printStackTrace(); + ErrorActivity.reportError(getActivity(), + failReason.getCause(), null, rootView, + ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE, + ServiceList.getNameOfService(info.service_id), imageUri, + R.string.could_not_load_thumbnails)); } @Override public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - videoThumbnail = loadedImage; + streamThumbnail = loadedImage; } @Override @@ -488,11 +333,8 @@ public class VideoItemDetailFragment extends Fragment { } if(info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) { imageLoader.displayImage(info.uploader_thumbnail_url, - uploaderThumb, displayImageOptions, new ThumbnailLoadingListener()); - } - if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty() && info.next_video != null) { - imageLoader.displayImage(info.next_video.thumbnail_url, - nextVideoThumb, displayImageOptions, new ThumbnailLoadingListener()); + uploaderThumb, displayImageOptions, + new ImageErrorLoadingListener(activity, rootView, info.service_id)); } } @@ -602,9 +444,9 @@ public class VideoItemDetailFragment extends Fragment { info.audio_streams.get(getPreferredAudioStreamId(info)); if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) { //internal music player: explicit intent - if (!BackgroundPlayer.isRunning && videoThumbnail != null) { + if (!BackgroundPlayer.isRunning && streamThumbnail != null) { ActivityCommunicator.getCommunicator() - .backgroundPlayerThumbnail = videoThumbnail; + .backgroundPlayerThumbnail = streamThumbnail; intent = new Intent(activity, BackgroundPlayer.class); intent.setAction(Intent.ACTION_VIEW); @@ -684,47 +526,14 @@ public class VideoItemDetailFragment extends Fragment { return 0; } - private void initSimilarVideos(final StreamInfo info, VideoInfoItemViewCreator videoItemViewCreator) { - LinearLayout similarLayout = (LinearLayout) activity.findViewById(R.id.similarVideosView); - ArrayList similar = new ArrayList<>(info.related_videos); - for (final StreamPreviewInfo item : similar) { - View similarView = videoItemViewCreator - .getViewFromVideoInfoItem(null, similarLayout, item); - - similarView.setClickable(true); - similarView.setFocusable(true); - int[] attrs = new int[]{R.attr.selectableItemBackground}; - TypedArray typedArray = activity.obtainStyledAttributes(attrs); - int backgroundResource = typedArray.getResourceId(0, 0); - similarView.setBackgroundResource(backgroundResource); - typedArray.recycle(); - - similarView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_UP) { - Intent detailIntent = new Intent(activity, VideoItemDetailActivity.class); - detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, item.webpage_url); - detailIntent.putExtra( - VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId); - startActivity(detailIntent); - return true; - } - return false; - } - }); - - similarLayout.addView(similarView); - ImageView rthumb = (ImageView)similarView.findViewById(R.id.itemThumbnailView); - imageLoader.displayImage(item.thumbnail_url, rthumb, - displayImageOptions, new ThumbnailLoadingListener()); - } + private void initSimilarVideos(final StreamInfo info) { + similarStreamsAdapter.addStreamItemList(info.related_streams); } private void onErrorBlockedByGema() { Button backgroundButton = (Button) - activity.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton); - ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); + activity.findViewById(R.id.detail_stream_thumbnail_window_background_button); + ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view); progressBar.setVisibility(View.GONE); thumbnailView.setImageBitmap(BitmapFactory.decodeResource( @@ -744,7 +553,7 @@ public class VideoItemDetailFragment extends Fragment { } private void onNotSpecifiedContentError() { - ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); + ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view); progressBar.setVisibility(View.GONE); thumbnailView.setImageBitmap(BitmapFactory.decodeResource( getResources(), R.drawable.not_available_monkey)); @@ -753,7 +562,7 @@ public class VideoItemDetailFragment extends Fragment { } private void onNotSpecifiedContentErrorWithMessage(int resourceId) { - ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); + ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view); progressBar.setVisibility(View.GONE); thumbnailView.setImageBitmap(BitmapFactory.decodeResource( getResources(), R.drawable.not_available_monkey)); @@ -779,16 +588,44 @@ public class VideoItemDetailFragment extends Fragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activity = (AppCompatActivity) getActivity(); - showNextVideoItem = PreferenceManager.getDefaultSharedPreferences(getActivity()) + showNextStreamItem = PreferenceManager.getDefaultSharedPreferences(getActivity()) .getBoolean(activity.getString(R.string.show_next_video_key), true); + + StreamInfoWorker siw = StreamInfoWorker.getInstance(); + siw.setOnStreamInfoReceivedListener(new StreamInfoWorker.OnStreamInfoReceivedListener() { + @Override + public void onReceive(StreamInfo info) { + updateInfo(info); + } + + @Override + public void onError(int messageId) { + postNewErrorToast(messageId); + } + + @Override + public void onBlockedByGemaError() { + onErrorBlockedByGema(); + } + + @Override + public void onContentErrorWithMessage(int messageId) { + onNotSpecifiedContentErrorWithMessage(messageId); + } + + @Override + public void onContentError() { + onNotSpecifiedContentError(); + } + }); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_videoitem_detail, container, false); - progressBar = (ProgressBar) rootView.findViewById(R.id.detailProgressBar); + rootView = inflater.inflate(R.layout.fragment_videoitem_detail, container, false); + progressBar = (ProgressBar) rootView.findViewById(R.id.detail_progress_bar); actionBarHandler = new ActionBarHandler(activity); actionBarHandler.setupNavMenu(activity); @@ -804,30 +641,25 @@ public class VideoItemDetailFragment extends Fragment { super.onActivityCreated(savedInstanceBundle); Activity a = getActivity(); if (android.os.Build.VERSION.SDK_INT < 18) { - playVideoButton = (FloatingActionButton) a.findViewById(R.id.playVideoButton); + playVideoButton = (FloatingActionButton) a.findViewById(R.id.play_video_button); } - thumbnailWindowLayout = a.findViewById(R.id.detailVideoThumbnailWindowLayout); + thumbnailWindowLayout = a.findViewById(R.id.detail_stream_thumbnail_window_layout); Button backgroundButton = (Button) - a.findViewById(R.id.detailVideoThumbnailWindowBackgroundButton); + a.findViewById(R.id.detail_stream_thumbnail_window_background_button); // Sometimes when this fragment is not visible it still gets initiated // then we must not try to access objects of this fragment. // Otherwise the applications would crash. if(backgroundButton != null) { - try { - streamingServiceId = getArguments().getInt(STREAMING_SERVICE); - StreamingService streamingService = ServiceList.getService(streamingServiceId); - Thread videoExtractorThread = new Thread(new VideoExtractorRunnable( - getArguments().getString(VIDEO_URL), streamingService)); + streamingServiceId = getArguments().getInt(STREAMING_SERVICE); + String videoUrl = getArguments().getString(VIDEO_URL); + StreamInfoWorker siw = StreamInfoWorker.getInstance(); + siw.search(streamingServiceId, videoUrl, getActivity()); - autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY); - videoExtractorThread.start(); - } catch (Exception e) { - e.printStackTrace(); - } + autoPlayEnabled = getArguments().getBoolean(AUTO_PLAY); if(Build.VERSION.SDK_INT >= 18) { - ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detailThumbnailView); + ImageView thumbnailView = (ImageView) activity.findViewById(R.id.detail_thumbnail_view); thumbnailView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { // This is used to synchronize the thumbnailWindowButton and the playVideoButton // inside the ScrollView with the actual size of the thumbnail. @@ -849,6 +681,17 @@ public class VideoItemDetailFragment extends Fragment { } }); } + + similarStreamsAdapter = new InfoListAdapter(getActivity(), rootView); + RecyclerView rv = (RecyclerView) getActivity().findViewById(R.id.similar_streams_view); + rv.setLayoutManager(new LinearLayoutManager(getActivity())); + rv.setAdapter(similarStreamsAdapter); + similarStreamsAdapter.setOnItemSelectedListener(new InfoListAdapter.OnItemSelectedListener() { + @Override + public void selected(String url) { + openStreamUrl(url); + } + }); } } @@ -946,23 +789,16 @@ public class VideoItemDetailFragment extends Fragment { this.onInvokeCreateOptionsMenuListener = listener; } - private void postNewErrorToast(Handler h, final int stringResource) { - h.post(new Runnable() { - @Override - public void run() { - Toast.makeText(VideoItemDetailFragment.this.getActivity(), - stringResource, Toast.LENGTH_LONG).show(); - } - }); + private void postNewErrorToast(final int stringResource) { + Toast.makeText(VideoItemDetailFragment.this.getActivity(), + stringResource, Toast.LENGTH_LONG).show(); } - private void postNewErrorToast(Handler h, final String message) { - h.post(new Runnable() { - @Override - public void run() { - Toast.makeText(VideoItemDetailFragment.this.getActivity(), - message, Toast.LENGTH_LONG).show(); - } - }); + private void openStreamUrl(String url) { + Intent detailIntent = new Intent(activity, VideoItemDetailActivity.class); + detailIntent.putExtra(VideoItemDetailFragment.VIDEO_URL, url); + detailIntent.putExtra( + VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId); + activity.startActivity(detailIntent); } } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index b9c5a7439..c7018ae1d 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -25,7 +25,6 @@ import android.widget.SeekBar; import android.widget.TextView; import org.schabi.newpipe.App; -import org.schabi.newpipe.NewPipeSettings; import org.schabi.newpipe.R; import java.io.File; diff --git a/app/src/main/java/org/schabi/newpipe/download/MainActivity.java b/app/src/main/java/org/schabi/newpipe/download/MainActivity.java index e6c583287..a62d645f7 100644 --- a/app/src/main/java/org/schabi/newpipe/download/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/MainActivity.java @@ -8,14 +8,12 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; -import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.support.v4.app.NavUtils; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -23,25 +21,16 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.widget.AdapterView; -import android.widget.Button; import android.widget.EditText; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; -import android.support.v7.widget.SearchView; import org.schabi.newpipe.ErrorActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.SettingsActivity; -import org.schabi.newpipe.VideoItemDetailActivity; -import org.schabi.newpipe.VideoItemListActivity; -import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.settings.SettingsActivity; import java.io.File; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; import java.util.Vector; import us.shandian.giga.get.DownloadManager; @@ -257,7 +246,7 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte switch (id) { case android.R.id.home: { - Intent intent = new Intent(this, VideoItemListActivity.class); + Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); NavUtils.navigateUpTo(this, intent); return true; @@ -268,7 +257,7 @@ public class MainActivity extends AppCompatActivity implements AdapterView.OnIte return true; } case R.id.action_report_error: { - ErrorActivity.reportError(MainActivity.this, new Vector(), + ErrorActivity.reportError(MainActivity.this, new Vector(), null, null, ErrorActivity.ErrorInfo.make(ErrorActivity.USER_REPORT, null, diff --git a/app/src/main/java/org/schabi/newpipe/extractor/ChannelExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/ChannelExtractor.java new file mode 100644 index 000000000..d8c931d08 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/ChannelExtractor.java @@ -0,0 +1,55 @@ +package org.schabi.newpipe.extractor; + +import java.io.IOException; + +/** + * Created by Christian Schabesberger on 25.07.16. + * + * Copyright (C) Christian Schabesberger 2016 + * ChannelExtractor.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 . + */ + +public abstract class ChannelExtractor { + private int serviceId; + private String url; + private UrlIdHandler urlIdHandler; + private Downloader downloader; + private StreamPreviewInfoCollector previewInfoCollector; + + public ChannelExtractor(UrlIdHandler urlIdHandler, String url, int page, Downloader dl, int serviceId) + throws ExtractionException, IOException { + this.serviceId = serviceId; + this.urlIdHandler = urlIdHandler; + previewInfoCollector = new StreamPreviewInfoCollector(urlIdHandler, serviceId); + } + + public String getUrl() { return url; } + public UrlIdHandler getUrlIdHandler() { return urlIdHandler; } + public Downloader getDownloader() { return downloader; } + public StreamPreviewInfoCollector getStreamPreviewInfoCollector() { + return previewInfoCollector; + } + + public abstract String getChannelName() throws ParsingException; + public abstract String getAvatarUrl() throws ParsingException; + public abstract String getBannerUrl() throws ParsingException; + public abstract String getFeedUrl() throws ParsingException; + public abstract StreamPreviewInfoCollector getStreams() throws ParsingException; + public abstract boolean hasNextPage() throws ParsingException; + public int getServiceId() { + return serviceId; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/ChannelInfo.java b/app/src/main/java/org/schabi/newpipe/extractor/ChannelInfo.java new file mode 100644 index 000000000..2f856dac0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/ChannelInfo.java @@ -0,0 +1,79 @@ +package org.schabi.newpipe.extractor; + +import android.util.Log; + +import java.util.List; +import java.util.Vector; + +/** + * Created by Christian Schabesberger on 31.07.16. + * + * Copyright (C) Christian Schabesberger 2016 + * ChannelInfo.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 . + */ + +public class ChannelInfo { + + + public void addException(Exception e) { + errors.add(e); + } + + public static ChannelInfo getInfo(ChannelExtractor extractor, Downloader dl) + throws ParsingException { + ChannelInfo info = new ChannelInfo(); + + // importand data + info.service_id = extractor.getServiceId(); + info.channel_name = extractor.getChannelName(); + info.hasNextPage = extractor.hasNextPage(); + + try { + info.avatar_url = extractor.getAvatarUrl(); + } catch (Exception e) { + info.errors.add(e); + } + try { + info.banner_url = extractor.getBannerUrl(); + } catch (Exception e) { + info.errors.add(e); + } + try { + info.feed_url = extractor.getFeedUrl(); + } catch(Exception e) { + info.errors.add(e); + } + try { + StreamPreviewInfoCollector c = extractor.getStreams(); + info.related_streams = c.getItemList(); + info.errors.addAll(c.getErrors()); + } catch(Exception e) { + info.errors.add(e); + } + + return info; + } + + public int service_id = -1; + public String channel_name = ""; + public String avatar_url = ""; + public String banner_url = ""; + public String feed_url = ""; + public List related_streams = null; + public boolean hasNextPage = false; + + public List errors = new Vector<>(); +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/Downloader.java b/app/src/main/java/org/schabi/newpipe/extractor/Downloader.java index a4cde400d..14475dba7 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/Downloader.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/Downloader.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.extractor; import java.io.IOException; +import java.util.Map; /** * Created by Christian Schabesberger on 28.01.16. @@ -32,6 +33,14 @@ public interface Downloader { * @throws IOException*/ String download(String siteUrl, String language) throws IOException; + /**Download the text file at the supplied URL as in download(String), + * but set the HTTP header field "Accept-Language" to the supplied string. + * @param siteUrl the URL of the text file to return the contents of + * @param customProperties set request header properties + * @return the contents of the specified text file + * @throws IOException*/ + String download(String siteUrl, Map customProperties) throws IOException; + /**Download (via HTTP) the text file located at the supplied URL, and return its contents. * Primarily intended for downloading web pages. * @param siteUrl the URL of the text file to download diff --git a/app/src/main/java/org/schabi/newpipe/extractor/SearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/SearchEngine.java index a9c01fc22..3b700f129 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/SearchEngine.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/SearchEngine.java @@ -33,7 +33,7 @@ public abstract class SearchEngine { private StreamPreviewInfoSearchCollector collector; - public SearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) { + public SearchEngine(UrlIdHandler urlIdHandler, int serviceId) { collector = new StreamPreviewInfoSearchCollector(urlIdHandler, serviceId); } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/SearchResult.java b/app/src/main/java/org/schabi/newpipe/extractor/SearchResult.java index 36150d68b..8feccad0c 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/SearchResult.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/SearchResult.java @@ -43,5 +43,5 @@ public class SearchResult { public String suggestion = ""; public List resultList = new Vector<>(); - public List errors = new Vector<>(); + public List errors = new Vector<>(); } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/ServiceList.java b/app/src/main/java/org/schabi/newpipe/extractor/ServiceList.java index 63ccc5cb2..9571819a9 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/ServiceList.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/ServiceList.java @@ -40,7 +40,7 @@ public class ServiceList { public static StreamingService[] getServices() { return services; } - public static StreamingService getService(int serviceId) throws ExtractionException { + public static StreamingService getService(int serviceId)throws ExtractionException { for(StreamingService s : services) { if(s.getServiceId() == serviceId) { return s; diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamExtractor.java index eddac4ce5..c3e8c6fcb 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/StreamExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamExtractor.java @@ -30,7 +30,7 @@ public abstract class StreamExtractor { private int serviceId; private String url; - private StreamUrlIdHandler urlIdHandler; + private UrlIdHandler urlIdHandler; private Downloader downloader; private StreamPreviewInfoCollector previewInfoCollector; @@ -55,7 +55,7 @@ public abstract class StreamExtractor { } } - public StreamExtractor(StreamUrlIdHandler urlIdHandler, String url, Downloader dl, int serviceId) { + public StreamExtractor(UrlIdHandler urlIdHandler, String url, Downloader dl, int serviceId) { this.serviceId = serviceId; this.urlIdHandler = urlIdHandler; previewInfoCollector = new StreamPreviewInfoCollector(urlIdHandler, serviceId); @@ -69,7 +69,7 @@ public abstract class StreamExtractor { return url; } - public StreamUrlIdHandler getUrlIdHandler() { + public UrlIdHandler getUrlIdHandler() { return urlIdHandler; } @@ -81,6 +81,7 @@ public abstract class StreamExtractor { public abstract String getTitle() throws ParsingException; public abstract String getDescription() throws ParsingException; public abstract String getUploader() throws ParsingException; + public abstract String getChannelUrl() throws ParsingException; public abstract int getLength() throws ParsingException; public abstract long getViewCount() throws ParsingException; public abstract String getUploadDate() throws ParsingException; diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java index cd79e463f..9c710114b 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamInfo.java @@ -85,12 +85,12 @@ public class StreamInfo extends AbstractVideoInfo { /* ---- importand data, withoug the video can't be displayed goes here: ---- */ // if one of these is not available an exception is ment to be thrown directly into the frontend. - StreamUrlIdHandler uiconv = extractor.getUrlIdHandler(); + UrlIdHandler uiconv = extractor.getUrlIdHandler(); streamInfo.service_id = extractor.getServiceId(); streamInfo.webpage_url = extractor.getPageUrl(); streamInfo.stream_type = extractor.getStreamType(); - streamInfo.id = uiconv.getVideoId(extractor.getPageUrl()); + streamInfo.id = uiconv.getId(extractor.getPageUrl()); streamInfo.title = extractor.getTitle(); streamInfo.age_limit = extractor.getAgeLimit(); @@ -188,6 +188,11 @@ public class StreamInfo extends AbstractVideoInfo { } catch(Exception e) { streamInfo.addException(e); } + try { + streamInfo.channel_url = extractor.getChannelUrl(); + } catch(Exception e) { + streamInfo.addException(e); + } try { streamInfo.description = extractor.getDescription(); } catch(Exception e) { @@ -248,7 +253,7 @@ public class StreamInfo extends AbstractVideoInfo { try { // get related videos StreamPreviewInfoCollector c = extractor.getRelatedVideos(); - streamInfo.related_videos = c.getItemList(); + streamInfo.related_streams = c.getItemList(); streamInfo.errors.addAll(c.getErrors()); } catch(Exception e) { streamInfo.addException(e); @@ -258,6 +263,7 @@ public class StreamInfo extends AbstractVideoInfo { } public String uploader_thumbnail_url = ""; + public String channel_url = ""; public String description = ""; public List video_streams = null; @@ -275,9 +281,9 @@ public class StreamInfo extends AbstractVideoInfo { public int dislike_count = -1; public String average_rating = ""; public StreamPreviewInfo next_video = null; - public List related_videos = null; + public List related_streams = null; //in seconds. some metadata is not passed using a StreamInfo object! public int start_position = 0; - public List errors = new Vector<>(); + public List errors = new Vector<>(); } \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoCollector.java index 013bbbc1d..676e0988c 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoCollector.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoCollector.java @@ -27,11 +27,11 @@ import java.util.Vector; public class StreamPreviewInfoCollector { private List itemList = new Vector<>(); - private List errors = new Vector<>(); - private StreamUrlIdHandler urlIdHandler; + private List errors = new Vector<>(); + private UrlIdHandler urlIdHandler; private int serviceId = -1; - public StreamPreviewInfoCollector(StreamUrlIdHandler handler, int serviceId) { + public StreamPreviewInfoCollector(UrlIdHandler handler, int serviceId) { urlIdHandler = handler; this.serviceId = serviceId; } @@ -40,7 +40,7 @@ public class StreamPreviewInfoCollector { return itemList; } - public List getErrors() { + public List getErrors() { return errors; } @@ -57,7 +57,7 @@ public class StreamPreviewInfoCollector { if (urlIdHandler == null) { throw new ParsingException("Error: UrlIdHandler not set"); } else if(!resultItem.webpage_url.isEmpty()) { - resultItem.id = (new YoutubeStreamUrlIdHandler()).getVideoId(resultItem.webpage_url); + resultItem.id = (new YoutubeStreamUrlIdHandler()).getId(resultItem.webpage_url); } resultItem.title = extractor.getTitle(); resultItem.stream_type = extractor.getStreamType(); diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoSearchCollector.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoSearchCollector.java index e0f02f461..905543be0 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoSearchCollector.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamPreviewInfoSearchCollector.java @@ -24,7 +24,7 @@ public class StreamPreviewInfoSearchCollector extends StreamPreviewInfoCollector private String suggestion = ""; - public StreamPreviewInfoSearchCollector(StreamUrlIdHandler handler, int serviceId) { + public StreamPreviewInfoSearchCollector(UrlIdHandler handler, int serviceId) { super(handler, serviceId); } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamingService.java b/app/src/main/java/org/schabi/newpipe/extractor/StreamingService.java index de9a4ebfe..1805b48db 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/StreamingService.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/StreamingService.java @@ -38,7 +38,10 @@ public abstract class StreamingService { public abstract StreamExtractor getExtractorInstance(String url, Downloader downloader) throws IOException, ExtractionException; public abstract SearchEngine getSearchEngineInstance(Downloader downloader); - public abstract StreamUrlIdHandler getUrlIdHandlerInstance(); + public abstract UrlIdHandler getUrlIdHandlerInstance(); + public abstract UrlIdHandler getChannelUrlIdHandlerInstance(); + public abstract ChannelExtractor getChannelExtractorInstance(String url, int page, Downloader downloader) + throws ExtractionException, IOException; public final int getServiceId() { return serviceId; diff --git a/app/src/main/java/org/schabi/newpipe/extractor/StreamUrlIdHandler.java b/app/src/main/java/org/schabi/newpipe/extractor/UrlIdHandler.java similarity index 82% rename from app/src/main/java/org/schabi/newpipe/extractor/StreamUrlIdHandler.java rename to app/src/main/java/org/schabi/newpipe/extractor/UrlIdHandler.java index 4f91ac80e..f38f60658 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/StreamUrlIdHandler.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/UrlIdHandler.java @@ -1,10 +1,10 @@ package org.schabi.newpipe.extractor; /** - * Created by Christian Schabesberger on 02.02.16. + * Created by Christian Schabesberger on 26.07.16. * * Copyright (C) Christian Schabesberger 2016 - * StreamUrlIdHandler.java is part of NewPipe. + * UrlIdHandler.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 @@ -20,9 +20,9 @@ package org.schabi.newpipe.extractor; * along with NewPipe. If not, see . */ -public interface StreamUrlIdHandler { - String getVideoUrl(String videoId); - String getVideoId(String siteUrl) throws ParsingException; +public interface UrlIdHandler { + String getUrl(String videoId); + String getId(String siteUrl) throws ParsingException; String cleanUrl(String siteUrl) throws ParsingException; /**When a VIEW_ACTION is caught this function will test if the url delivered within the calling diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelExtractor.java new file mode 100644 index 000000000..d4653d811 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelExtractor.java @@ -0,0 +1,331 @@ +package org.schabi.newpipe.extractor.services.youtube; + + + +import org.json.JSONException; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.schabi.newpipe.extractor.AbstractVideoInfo; +import org.schabi.newpipe.extractor.ChannelExtractor; +import org.schabi.newpipe.extractor.Downloader; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.Parser; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.StreamPreviewInfoCollector; +import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor; +import org.schabi.newpipe.extractor.UrlIdHandler; + + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Christian Schabesberger on 25.07.16. + * + * Copyright (C) Christian Schabesberger 2016 + * YoutubeChannelExtractor.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 . + */ + +public class YoutubeChannelExtractor extends ChannelExtractor { + + private static final String TAG = YoutubeChannelExtractor.class.toString(); + + // private CSSOMParser cssParser = new CSSOMParser(new SACParserCSS3()); + + private Downloader downloader; + private Document doc = null; + + private boolean isAjaxPage = false; + private static String userUrl = ""; + private static String channelName = ""; + private static String avatarUrl = ""; + private static String bannerUrl = ""; + private static String feedUrl = ""; + // the fist page is html all other pages are ajax. Every new page can be requested by sending + // this request url. + private static String nextPageUrl = ""; + + public YoutubeChannelExtractor(UrlIdHandler urlIdHandler, String url, int page, Downloader dl, int serviceId) + throws ExtractionException, IOException { + super(urlIdHandler, url, page, dl, serviceId); + + url = urlIdHandler.cleanUrl(url) ; //+ "/video?veiw=0&flow=list&sort=dd"; + downloader = dl; + + if(page == 0) { + if (isUserUrl(url)) { + userUrl = url; + } else { + // we first need to get the user url. Otherwise we can't find videos + String channelPageContent = downloader.download(url); + Document channelDoc = Jsoup.parse(channelPageContent, url); + userUrl = getUserUrl(channelDoc); + } + + userUrl = userUrl + "/videos?veiw=0&flow=list&sort=dd&live_view=10000"; + String pageContent = downloader.download(userUrl); + doc = Jsoup.parse(pageContent, userUrl); + nextPageUrl = getNextPageUrl(doc); + isAjaxPage = false; + } else { + String ajaxDataRaw = downloader.download(nextPageUrl); + JSONObject ajaxData; + try { + ajaxData = new JSONObject(ajaxDataRaw); + String htmlDataRaw = ajaxData.getString("content_html"); + doc = Jsoup.parse(htmlDataRaw, nextPageUrl); + + String nextPageHtmlDataRaw = ajaxData.getString("load_more_widget_html"); + if(!nextPageHtmlDataRaw.isEmpty()) { + Document nextPageData = Jsoup.parse(nextPageHtmlDataRaw, nextPageUrl); + nextPageUrl = getNextPageUrl(nextPageData); + } else { + nextPageUrl = ""; + } + } catch (JSONException e) { + throw new ParsingException("Could not parse json data for next page", e); + } + isAjaxPage = true; + } + } + + @Override + public String getChannelName() throws ParsingException { + try { + if(!isAjaxPage) { + channelName = doc.select("span[class=\"qualified-channel-title-text\"]").first() + .select("a").first().text(); + } + return channelName; + } catch(Exception e) { + throw new ParsingException("Could not get channel name"); + } + } + + @Override + public String getAvatarUrl() throws ParsingException { + try { + if(!isAjaxPage) { + avatarUrl = doc.select("img[class=\"channel-header-profile-image\"]") + .first().attr("abs:src"); + } + return avatarUrl; + } catch(Exception e) { + throw new ParsingException("Could not get avatar", e); + } + } + + @Override + public String getBannerUrl() throws ParsingException { + try { + if(!isAjaxPage) { + Element el = doc.select("div[id=\"gh-banner\"]").first().select("style").first(); + String cssContent = el.html(); + String url = "https:" + Parser.matchGroup1("url\\((.*)\\)", cssContent); + if (url.contains("s.ytimg.com")) { + bannerUrl = null; + } else { + bannerUrl = url; + } + } + return bannerUrl; + } catch(Exception e) { + throw new ParsingException("Could not get Banner", e); + } + } + + @Override + public StreamPreviewInfoCollector getStreams() throws ParsingException { + StreamPreviewInfoCollector collector = getStreamPreviewInfoCollector(); + Element ul = null; + if(isAjaxPage) { + ul = doc.select("body").first(); + } else { + ul = doc.select("ul[id=\"browse-items-primary\"]").first(); + } + + for(final Element li : ul.children()) { + if (li.select("div[class=\"feed-item-dismissable\"]").first() != null) { + collector.commit(new StreamPreviewInfoExtractor() { + @Override + public AbstractVideoInfo.StreamType getStreamType() throws ParsingException { + return AbstractVideoInfo.StreamType.VIDEO_STREAM; + } + + @Override + public String getWebPageUrl() throws ParsingException { + try { + Element el = li.select("div[class=\"feed-item-dismissable\"]").first(); + Element dl = el.select("h3").first().select("a").first(); + return dl.attr("abs:href"); + } catch (Exception e) { + throw new ParsingException("Could not get web page url for the video", e); + } + } + + @Override + public String getTitle() throws ParsingException { + try { + Element el = li.select("div[class=\"feed-item-dismissable\"]").first(); + Element dl = el.select("h3").first().select("a").first(); + return dl.text(); + } catch (Exception e) { + throw new ParsingException("Could not get title", e); + } + } + + @Override + public int getDuration() throws ParsingException { + try { + return YoutubeParsingHelper.parseDurationString( + li.select("span[class=\"video-time\"]").first().text()); + } catch(Exception e) { + if(isLiveStream(li)) { + // -1 for no duration + return -1; + } else { + throw new ParsingException("Could not get Duration: " + getTitle(), e); + } + } + } + + @Override + public String getUploader() throws ParsingException { + return getChannelName(); + } + + @Override + public String getUploadDate() throws ParsingException { + try { + return li.select("div[class=\"yt-lockup-meta\"]").first() + .select("li").first() + .text(); + } catch(Exception e) { + throw new ParsingException("Could not get uplaod date", e); + } + } + + @Override + public long getViewCount() throws ParsingException { + String output; + String input; + try { + input = li.select("div[class=\"yt-lockup-meta\"]").first() + .select("li").get(1) + .text(); + } catch (IndexOutOfBoundsException e) { + if(isLiveStream(li)) { + // -1 for no view count + return -1; + } else { + throw new ParsingException( + "Could not parse yt-lockup-meta although available: " + getTitle(), e); + } + } + + output = Parser.matchGroup1("([0-9,\\. ]*)", input) + .replace(" ", "") + .replace(".", "") + .replace(",", ""); + + try { + return Long.parseLong(output); + } catch (NumberFormatException e) { + // if this happens the video probably has no views + if(!input.isEmpty()) { + return 0; + } else { + throw new ParsingException("Could not handle input: " + input, e); + } + } + } + + @Override + public String getThumbnailUrl() throws ParsingException { + try { + String url; + Element te = li.select("span[class=\"yt-thumb-clip\"]").first() + .select("img").first(); + url = te.attr("abs:src"); + // Sometimes youtube sends links to gif files which somehow seem to not exist + // anymore. Items with such gif also offer a secondary image source. So we are going + // to use that if we've caught such an item. + if (url.contains(".gif")) { + url = te.attr("abs:data-thumb"); + } + return url; + } catch (Exception e) { + throw new ParsingException("Could not get thumbnail url", e); + } + } + + private boolean isLiveStream(Element item) { + Element bla = item.select("span[class*=\"yt-badge-live\"]").first(); + + if(bla == null) { + // sometimes livestreams dont have badges but sill are live streams + // if video time is not available we most likly have an offline livestream + if(item.select("span[class*=\"video-time\"]").first() == null) { + return true; + } + } + return bla != null; + } + }); + } + } + + return collector; + } + + @Override + public String getFeedUrl() throws ParsingException { + try { + if(!isAjaxPage) { + feedUrl = doc.select("link[title=\"RSS\"]").first().attr("abs:href"); + } + return feedUrl; + } catch(Exception e) { + throw new ParsingException("Could not get feed url", e); + } + } + + @Override + public boolean hasNextPage() throws ParsingException { + return !nextPageUrl.isEmpty(); + } + + private String getUserUrl(Document d) throws ParsingException { + return d.select("span[class=\"qualified-channel-title-text\"]").first() + .select("a").first().attr("abs:href"); + } + + private boolean isUserUrl(String url) throws ParsingException { + return url.contains("/user/"); + } + + private String getNextPageUrl(Document d) throws ParsingException { + try { + Element button = d.select("button[class*=\"yt-uix-load-more\"]").first(); + return button.attr("abs:data-uix-load-more-href"); + } catch(Exception e) { + throw new ParsingException("could not load next page url", e); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelUrlIdHandler.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelUrlIdHandler.java new file mode 100644 index 000000000..5f6f85ae6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeChannelUrlIdHandler.java @@ -0,0 +1,47 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import org.schabi.newpipe.extractor.Parser; +import org.schabi.newpipe.extractor.ParsingException; +import org.schabi.newpipe.extractor.UrlIdHandler; + +/** + * Created by Christian Schabesberger on 25.07.16. + * + * Copyright (C) Christian Schabesberger 2016 + * YoutubeChannelUrlIdHandler.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 . + */ + +public class YoutubeChannelUrlIdHandler implements UrlIdHandler { + + public String getUrl(String channelId) { + return "https://www.youtube.com/" + channelId; + } + + public String getId(String siteUrl) throws ParsingException { + return Parser.matchGroup1("/(user/[A-Za-z0-9_-]*|channel/[A-Za-z0-9_-]*)", siteUrl); + } + + public String cleanUrl(String siteUrl) throws ParsingException { + return getUrl(getId(siteUrl)); + } + + public boolean acceptUrl(String videoUrl) { + return (videoUrl.contains("youtube") || + videoUrl.contains("youtu.be")) && + ( videoUrl.contains("/user/") || + videoUrl.contains("/channel/")); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java index 249814248..5e84eac77 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeSearchEngine.java @@ -14,7 +14,7 @@ import org.schabi.newpipe.extractor.SearchEngine; import org.schabi.newpipe.extractor.StreamPreviewInfoCollector; import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor; import org.schabi.newpipe.extractor.StreamPreviewInfoSearchCollector; -import org.schabi.newpipe.extractor.StreamUrlIdHandler; +import org.schabi.newpipe.extractor.UrlIdHandler; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; @@ -55,7 +55,7 @@ public class YoutubeSearchEngine extends SearchEngine { private static final String TAG = YoutubeSearchEngine.class.toString(); public static final String CHARSET_UTF_8 = "UTF-8"; - public YoutubeSearchEngine(StreamUrlIdHandler urlIdHandler, int serviceId) { + public YoutubeSearchEngine(UrlIdHandler urlIdHandler, int serviceId) { super(urlIdHandler, serviceId); } @@ -91,7 +91,6 @@ public class YoutubeSearchEngine extends SearchEngine { site = downloader.download(url); } - Document doc = Jsoup.parse(site, url); Element list = doc.select("ol[class=\"item-section\"]").first(); diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java index f2d56bcab..9c2198926 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeService.java @@ -1,10 +1,11 @@ package org.schabi.newpipe.extractor.services.youtube; +import org.schabi.newpipe.extractor.ChannelExtractor; import org.schabi.newpipe.extractor.ExtractionException; import org.schabi.newpipe.extractor.Downloader; import org.schabi.newpipe.extractor.StreamExtractor; import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.StreamUrlIdHandler; +import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.SearchEngine; import java.io.IOException; @@ -45,7 +46,7 @@ public class YoutubeService extends StreamingService { @Override public StreamExtractor getExtractorInstance(String url, Downloader downloader) throws ExtractionException, IOException { - StreamUrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler(); + UrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler(); if(urlIdHandler.acceptUrl(url)) { return new YoutubeStreamExtractor(urlIdHandler, url, downloader, getServiceId()); } @@ -59,7 +60,18 @@ public class YoutubeService extends StreamingService { } @Override - public StreamUrlIdHandler getUrlIdHandlerInstance() { + public UrlIdHandler getUrlIdHandlerInstance() { return new YoutubeStreamUrlIdHandler(); } + + @Override + public UrlIdHandler getChannelUrlIdHandlerInstance() { + return new YoutubeChannelUrlIdHandler(); + } + + @Override + public ChannelExtractor getChannelExtractorInstance(String url, int page, Downloader downloader) + throws ExtractionException, IOException { + return new YoutubeChannelExtractor(getChannelUrlIdHandlerInstance(), url, page, downloader, getServiceId()); + } } diff --git a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java index afdaff3a9..aac94b355 100644 --- a/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeStreamExtractor.java @@ -15,10 +15,9 @@ import org.schabi.newpipe.extractor.Downloader; import org.schabi.newpipe.extractor.Parser; import org.schabi.newpipe.extractor.ParsingException; import org.schabi.newpipe.extractor.StreamInfo; -import org.schabi.newpipe.extractor.StreamPreviewInfo; import org.schabi.newpipe.extractor.StreamPreviewInfoCollector; import org.schabi.newpipe.extractor.StreamPreviewInfoExtractor; -import org.schabi.newpipe.extractor.StreamUrlIdHandler; +import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.StreamExtractor; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.VideoStream; @@ -183,12 +182,12 @@ public class YoutubeStreamExtractor extends StreamExtractor { // cached values private static volatile String decryptionCode = ""; - StreamUrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler(); + UrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler(); String pageUrl = ""; private Downloader downloader; - public YoutubeStreamExtractor(StreamUrlIdHandler urlIdHandler, String pageUrl, + public YoutubeStreamExtractor(UrlIdHandler urlIdHandler, String pageUrl, Downloader dl, int serviceId) throws ExtractionException, IOException { super(urlIdHandler ,pageUrl, dl, serviceId); @@ -203,7 +202,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { // Check if the video is age restricted if (pageContent.contains(". */ -public class YoutubeStreamUrlIdHandler implements StreamUrlIdHandler { +public class YoutubeStreamUrlIdHandler implements UrlIdHandler { @SuppressWarnings("WeakerAccess") @Override - public String getVideoUrl(String videoId) { + public String getUrl(String videoId) { return "https://www.youtube.com/watch?v=" + videoId; } @SuppressWarnings("WeakerAccess") @Override - public String getVideoId(String url) throws ParsingException, IllegalArgumentException { + public String getId(String url) throws ParsingException, IllegalArgumentException { if(url.isEmpty()) { throw new IllegalArgumentException("The url parameter should not be empty"); @@ -81,7 +81,7 @@ public class YoutubeStreamUrlIdHandler implements StreamUrlIdHandler { } public String cleanUrl(String complexUrl) throws ParsingException { - return getVideoUrl(getVideoId(complexUrl)); + return getUrl(getId(complexUrl)); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java new file mode 100644 index 000000000..0e3374dcc --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemHolder.java @@ -0,0 +1,34 @@ +package org.schabi.newpipe.info_list; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import org.schabi.newpipe.R; + +/** + * Created by the-scrabi on 01.08.16. + */ +public class InfoItemHolder extends RecyclerView.ViewHolder { + + public ImageView itemThumbnailView; + public TextView itemVideoTitleView, + itemUploaderView, + itemDurationView, + itemUploadDateView, + itemViewCountView; + public Button itemButton; + + public InfoItemHolder(View v) { + super(v); + itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView); + itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); + itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView); + itemDurationView = (TextView) v.findViewById(R.id.itemDurationView); + itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView); + itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView); + itemButton = (Button) v.findViewById(R.id.item_button); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/VideoInfoItemViewCreator.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java similarity index 55% rename from app/src/main/java/org/schabi/newpipe/VideoInfoItemViewCreator.java rename to app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index 4b7c3c332..365ed246e 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoInfoItemViewCreator.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -1,73 +1,79 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.info_list; +import android.app.Activity; +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 com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; +import org.schabi.newpipe.ImageErrorLoadingListener; +import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.AbstractVideoInfo; import org.schabi.newpipe.extractor.StreamPreviewInfo; +import java.util.List; +import java.util.Vector; + /** - * Created by Christian Schabesberger on 24.10.15. - * - * Copyright (C) Christian Schabesberger 2015 - * VideoInfoItemViewCreator.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 . + * Created by the-scrabi on 01.08.16. */ +public class InfoListAdapter extends RecyclerView.Adapter { -public class VideoInfoItemViewCreator { - private final LayoutInflater inflater; - private ImageLoader imageLoader = ImageLoader.getInstance(); - private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build(); - - public VideoInfoItemViewCreator(LayoutInflater inflater) { - this.inflater = inflater; + public interface OnItemSelectedListener { + void selected(String url); } - public View getViewFromVideoInfoItem(View convertView, ViewGroup parent, StreamPreviewInfo info) { - ViewHolder holder; + private Activity activity = null; + private View rootView = null; + private List streamList = new Vector<>(); + private ImageLoader imageLoader = ImageLoader.getInstance(); + private DisplayImageOptions displayImageOptions = + new DisplayImageOptions.Builder().cacheInMemory(true).build(); + private OnItemSelectedListener onItemSelectedListener; - // generate holder - if(convertView == null) { - convertView = inflater.inflate(R.layout.video_item, parent, false); - holder = new ViewHolder(); - holder.itemThumbnailView = (ImageView) convertView.findViewById(R.id.itemThumbnailView); - holder.itemVideoTitleView = (TextView) convertView.findViewById(R.id.itemVideoTitleView); - holder.itemUploaderView = (TextView) convertView.findViewById(R.id.itemUploaderView); - holder.itemDurationView = (TextView) convertView.findViewById(R.id.itemDurationView); - holder.itemUploadDateView = (TextView) convertView.findViewById(R.id.itemUploadDateView); - holder.itemViewCountView = (TextView) convertView.findViewById(R.id.itemViewCountView); - convertView.setTag(holder); - } else { - holder = (ViewHolder) convertView.getTag(); + + + public InfoListAdapter(Activity a, View rootView) { + activity = a; + this.rootView = rootView; + } + + public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) { + this.onItemSelectedListener = onItemSelectedListener; + } + + public void addStreamItemList(List videos) { + if(videos!= null) { + streamList.addAll(videos); + notifyDataSetChanged(); } + } - // fill with information + public void clearSteamItemList() { + streamList = new Vector<>(); + notifyDataSetChanged(); + } - /* - if(info.thumbnail == null) { - holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail); - } else { - holder.itemThumbnailView.setImageBitmap(info.thumbnail); - } - */ + @Override + public int getItemCount() { + return streamList.size(); + } + + @Override + public InfoItemHolder onCreateViewHolder(ViewGroup parent, int i) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.video_item, parent, false); + + return new InfoItemHolder(itemView); + } + + @Override + public void onBindViewHolder(InfoItemHolder holder, int i) { + final StreamPreviewInfo info = streamList.get(i); + // fill holder with information holder.itemVideoTitleView.setText(info.title); if(info.uploader != null && !info.uploader.isEmpty()) { holder.itemUploaderView.setText(info.uploader); @@ -94,18 +100,22 @@ public class VideoInfoItemViewCreator { holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail); if(info.thumbnail_url != null && !info.thumbnail_url.isEmpty()) { - imageLoader.displayImage(info.thumbnail_url, holder.itemThumbnailView, displayImageOptions); + imageLoader.displayImage(info.thumbnail_url, + holder.itemThumbnailView, + displayImageOptions, + new ImageErrorLoadingListener(activity, rootView, info.service_id)); } - return convertView; + holder.itemButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onItemSelectedListener.selected(info.webpage_url); + } + }); } - private class ViewHolder { - public ImageView itemThumbnailView; - public TextView itemVideoTitleView, itemUploaderView, itemDurationView, itemUploadDateView, itemViewCountView; - } - private String shortViewCount(Long viewCount){ + public static String shortViewCount(Long viewCount){ if(viewCount >= 1000000000){ return Long.toString(viewCount/1000000000)+"B views"; }else if(viewCount>=1000000){ diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index e283d5e05..5b4f246d0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -23,8 +23,8 @@ import android.widget.Toast; import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; -import org.schabi.newpipe.VideoItemDetailActivity; -import org.schabi.newpipe.VideoItemDetailFragment; +import org.schabi.newpipe.detail.VideoItemDetailActivity; +import org.schabi.newpipe.detail.VideoItemDetailFragment; import java.io.IOException; diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java new file mode 100644 index 000000000..99d2a57dd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java @@ -0,0 +1,265 @@ +package org.schabi.newpipe.search_fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.ProgressBar; +import android.widget.Toast; + +import org.schabi.newpipe.ErrorActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.detail.VideoItemDetailActivity; +import org.schabi.newpipe.detail.VideoItemDetailFragment; +import org.schabi.newpipe.extractor.SearchResult; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.info_list.InfoListAdapter; + +/** + * Created by Christian Schabesberger on 02.08.16. + */ + +public class SearchInfoItemFragment extends Fragment { + + private static final String TAG = SearchInfoItemFragment.class.toString(); + + public class SearchQueryListener implements SearchView.OnQueryTextListener { + + @Override + public boolean onQueryTextSubmit(String query) { + Activity a = getActivity(); + try { + searchQuery = query; + search(query); + + // hide virtual keyboard + InputMethodManager inputManager = + (InputMethodManager) a.getSystemService(Context.INPUT_METHOD_SERVICE); + try { + //noinspection ConstantConditions + inputManager.hideSoftInputFromWindow( + a.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } catch(NullPointerException e) { + e.printStackTrace(); + ErrorActivity.reportError(a, e, null, + a.findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + ServiceList.getNameOfService(streamingServiceId), + "Could not get widget with focus", R.string.general_error)); + } + // clear focus + // 1. to not open up the keyboard after switching back to this + // 2. It's a workaround to a seeming bug by the Android OS it self, causing + // onQueryTextSubmit to trigger twice when focus is not cleared. + // See: http://stackoverflow.com/questions/17874951/searchview-onquerytextsubmit-runs-twice-while-i-pressed-once + a.getCurrentFocus().clearFocus(); + } catch(Exception e) { + e.printStackTrace(); + } + View bg = a.findViewById(R.id.mainBG); + bg.setVisibility(View.GONE); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + if(!newText.isEmpty()) { + searchSuggestions(newText); + } + return true; + } + } + + private int streamingServiceId = -1; + private String searchQuery = ""; + private boolean isLoading = false; + + private ProgressBar loadingIndicator = null; + private SearchView searchView = null; + private int pageNumber = 0; + private SuggestionListAdapter suggestionListAdapter = null; + private InfoListAdapter infoListAdapter = null; + private LinearLayoutManager streamInfoListLayoutManager = null; + private RecyclerView recyclerView = null; + + // savedInstanceBundle arguments + private static final String QUERY = "query"; + private static final String STREAMING_SERVICE = "streaming_service"; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public SearchInfoItemFragment() { + } + + // TODO: Customize parameter initialization + @SuppressWarnings("unused") + public static SearchInfoItemFragment newInstance(int columnCount) { + SearchInfoItemFragment fragment = new SearchInfoItemFragment(); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if(savedInstanceState != null) { + searchQuery = savedInstanceState.getString(QUERY); + streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE); + } else { + try { + streamingServiceId = ServiceList.getIdOfService("Youtube"); + } catch(Exception e) { + e.printStackTrace(); + ErrorActivity.reportError(getActivity(), e, null, + getActivity().findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + ServiceList.getNameOfService(streamingServiceId), + "", R.string.general_error)); + } + } + + SearchWorker sw = SearchWorker.getInstance(); + sw.setSearchWorkerResultListner(new SearchWorker.SearchWorkerResultListner() { + @Override + public void onResult(SearchResult result) { + infoListAdapter.addStreamItemList(result.resultList); + isLoading = false; + loadingIndicator.setVisibility(View.GONE); + } + + @Override + public void onNothingFound(int stringResource) { + //setListShown(true); + Toast.makeText(getActivity(), getString(stringResource), + Toast.LENGTH_SHORT).show(); + isLoading = false; + loadingIndicator.setVisibility(View.GONE); + } + + @Override + public void onError(String message) { + //setListShown(true); + Toast.makeText(getActivity(), message, + Toast.LENGTH_LONG).show(); + isLoading = false; + loadingIndicator.setVisibility(View.GONE); + } + }); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_searchinfoitem, container, false); + + Context context = view.getContext(); + loadingIndicator = (ProgressBar) view.findViewById(R.id.progressBar); + recyclerView = (RecyclerView) view.findViewById(R.id.list); + streamInfoListLayoutManager = new LinearLayoutManager(context); + recyclerView.setLayoutManager(streamInfoListLayoutManager); + + infoListAdapter = new InfoListAdapter(getActivity(), + getActivity().findViewById(android.R.id.content)); + infoListAdapter.setOnItemSelectedListener(new InfoListAdapter.OnItemSelectedListener() { + @Override + public void selected(String url) { + Intent i = new Intent(getActivity(), VideoItemDetailActivity.class); + i.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId); + i.putExtra(VideoItemDetailFragment.VIDEO_URL, url); + getActivity().startActivity(i); + } + }); + recyclerView.setAdapter(infoListAdapter); + + recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + int pastVisiblesItems, visibleItemCount, totalItemCount; + super.onScrolled(recyclerView, dx, dy); + if(dy > 0) //check for scroll down + { + visibleItemCount = streamInfoListLayoutManager.getChildCount(); + totalItemCount = streamInfoListLayoutManager.getItemCount(); + pastVisiblesItems = streamInfoListLayoutManager.findFirstVisibleItemPosition(); + + if ( (visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading) + { + pageNumber++; + search(searchQuery, pageNumber); + } + } + } + }); + + return view; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + @Override + public void onDetach() { + super.onDetach(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.search_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + searchView = (SearchView) searchItem.getActionView(); + setupSearchView(searchView); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return super.onOptionsItemSelected(item); + } + + private void setupSearchView(SearchView searchView) { + suggestionListAdapter = new SuggestionListAdapter(getActivity()); + searchView.setSuggestionsAdapter(suggestionListAdapter); + searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView, suggestionListAdapter)); + searchView.setOnQueryTextListener(new SearchQueryListener()); + if(searchQuery != null && !searchQuery.isEmpty()) { + searchView.setQuery(searchQuery, false); + searchView.setIconifiedByDefault(false); + } + } + + private void search(String query) { + infoListAdapter.clearSteamItemList(); + pageNumber = 0; + search(query, pageNumber); + loadingIndicator.setVisibility(View.VISIBLE); + } + + private void search(String query, int page) { + isLoading = true; + SearchWorker sw = SearchWorker.getInstance(); + sw.search(streamingServiceId, query, page, getActivity()); + } + + private void searchSuggestions(String query) { + SuggestionSearchRunnable suggestionSearchRunnable = + new SuggestionSearchRunnable(streamingServiceId, query, getActivity(), suggestionListAdapter); + Thread suggestionThread = new Thread(suggestionSearchRunnable); + suggestionThread.start(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java new file mode 100644 index 000000000..833af8521 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java @@ -0,0 +1,32 @@ +package org.schabi.newpipe.search_fragment; + +import android.support.v7.widget.SearchView; + +/** + * Created by the-scrabi on 02.08.16. + */ + +public class SearchSuggestionListener implements SearchView.OnSuggestionListener{ + + private SearchView searchView; + private SuggestionListAdapter adapter; + + public SearchSuggestionListener(SearchView searchView, SuggestionListAdapter adapter) { + this.searchView = searchView; + this.adapter = adapter; + } + + @Override + public boolean onSuggestionSelect(int position) { + String suggestion = adapter.getSuggestion(position); + searchView.setQuery(suggestion,true); + return false; + } + + @Override + public boolean onSuggestionClick(int position) { + String suggestion = adapter.getSuggestion(position); + searchView.setQuery(suggestion,true); + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java new file mode 100644 index 000000000..d14cdbd41 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java @@ -0,0 +1,174 @@ +package org.schabi.newpipe.search_fragment; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import org.schabi.newpipe.Downloader; +import org.schabi.newpipe.ErrorActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.SearchEngine; +import org.schabi.newpipe.extractor.SearchResult; +import org.schabi.newpipe.extractor.ServiceList; + +import java.io.IOException; + +/** + * Created by the-scrabi on 02.08.16. + */ + + +public class SearchWorker { + private static final String TAG = SearchWorker.class.toString(); + + public interface SearchWorkerResultListner { + void onResult(SearchResult result); + void onNothingFound(final int stringResource); + void onError(String message); + } + + private class ResultRunnable implements Runnable { + private final SearchResult result; + private int requestId = 0; + public ResultRunnable(SearchResult result, int requestId) { + this.result = result; + this.requestId = requestId; + } + @Override + public void run() { + if(this.requestId == SearchWorker.this.requestId) { + searchWorkerResultListner.onResult(result); + } + } + } + + private class SearchRunnable implements Runnable { + public static final String YOUTUBE = "Youtube"; + private final String query; + private final int page; + final Handler h = new Handler(); + private volatile boolean runs = true; + private Activity a = null; + private int serviceId = -1; + public SearchRunnable(int serviceId, String query, int page, Activity activity, int requestId) { + this.serviceId = serviceId; + this.query = query; + this.page = page; + this.a = activity; + } + void terminate() { + runs = false; + } + @Override + public void run() { + SearchResult result = null; + SearchEngine engine = null; + + try { + engine = ServiceList.getService(serviceId) + .getSearchEngineInstance(new Downloader()); + } catch(ExtractionException e) { + ErrorActivity.reportError(h, a, e, null, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + Integer.toString(serviceId), query, R.string.general_error)); + return; + } + + try { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(a); + String searchLanguageKey = a.getString(R.string.search_language_key); + String searchLanguage = sp.getString(searchLanguageKey, + a.getString(R.string.default_language_value)); + result = SearchResult + .getSearchResult(engine, query, page, searchLanguage, new Downloader()); + + if(runs) { + h.post(new ResultRunnable(result, requestId)); + } + + // look for errors during extraction + // soft errors: + if(result != null && + !result.errors.isEmpty()) { + Log.e(TAG, "OCCURRED ERRORS DURING SEARCH EXTRACTION:"); + for(Throwable e : result.errors) { + e.printStackTrace(); + Log.e(TAG, "------"); + } + + View rootView = a.findViewById(android.R.id.content); + ErrorActivity.reportError(h, a, result.errors, null, rootView, + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + /* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.light_parsing_error)); + + } + // hard errors: + } catch(IOException e) { + h.post(new Runnable() { + @Override + public void run() { + searchWorkerResultListner.onNothingFound(R.string.network_error); + } + }); + e.printStackTrace(); + } catch(final SearchEngine.NothingFoundException e) { + h.post(new Runnable() { + @Override + public void run() { + searchWorkerResultListner.onError(e.getMessage()); + } + }); + } catch(ExtractionException e) { + ErrorActivity.reportError(h, a, e, null, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + /* todo: this shoudl not be assigned static */ + YOUTUBE, query, R.string.parsing_error)); + //postNewErrorToast(h, R.string.parsing_error); + e.printStackTrace(); + + } catch(Exception e) { + ErrorActivity.reportError(h, a, e, null, null, + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + /* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.general_error)); + + e.printStackTrace(); + } + } + } + + private static SearchWorker searchWorker = null; + private SearchWorkerResultListner searchWorkerResultListner = null; + private SearchRunnable runnable = null; + private int requestId = 0; //prevents running requests that have already ben expired + + public static SearchWorker getInstance() { + return searchWorker == null ? (searchWorker = new SearchWorker()) : searchWorker; + } + + public void setSearchWorkerResultListner(SearchWorkerResultListner listener) { + searchWorkerResultListner = listener; + } + + private SearchWorker() { + + } + + public void search(int serviceId, String query, int page, Activity a) { + if(runnable != null) { + terminate(); + } + runnable = new SearchRunnable(serviceId, query, page, a, requestId); + Thread thread = new Thread(runnable); + thread.start(); + } + + public void terminate() { + requestId++; + runnable.terminate(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java similarity index 66% rename from app/src/main/java/org/schabi/newpipe/SuggestionListAdapter.java rename to app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java index 10ba5efb0..5a8ae2ce7 100644 --- a/app/src/main/java/org/schabi/newpipe/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.search_fragment; import android.content.Context; import android.database.Cursor; @@ -9,27 +9,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import java.util.ArrayList; import java.util.List; /** - * Created by Madiyar on 23.02.2016. - * - * Copyright (C) Christian Schabesberger 2015 - * 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 . + * Created by the-scrabi on 02.08.16. */ public class SuggestionListAdapter extends CursorAdapter { @@ -80,4 +63,4 @@ public class SuggestionListAdapter extends CursorAdapter { private class ViewHolder { public TextView suggestionTitle; } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java new file mode 100644 index 000000000..94308200d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java @@ -0,0 +1,90 @@ +package org.schabi.newpipe.search_fragment; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import org.schabi.newpipe.Downloader; +import org.schabi.newpipe.ErrorActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.ExtractionException; +import org.schabi.newpipe.extractor.SearchEngine; +import org.schabi.newpipe.extractor.ServiceList; + +import java.io.IOException; +import java.util.List; + +/** + * Created by the-scrabi on 02.08.16. + */ + +public class SuggestionSearchRunnable implements Runnable{ + + private class SuggestionResultRunnable implements Runnable{ + + private List suggestions; + private SuggestionListAdapter adapter; + + private SuggestionResultRunnable(List suggestions, SuggestionListAdapter adapter) { + this.suggestions = suggestions; + this.adapter = adapter; + } + + @Override + public void run() { + adapter.updateAdapter(suggestions); + } + } + + private final int serviceId; + private final String query; + final Handler h = new Handler(); + private Activity a = null; + private SuggestionListAdapter adapter; + public SuggestionSearchRunnable(int serviceId, String query, + Activity activity, SuggestionListAdapter adapter) { + this.serviceId = serviceId; + this.query = query; + this.a = activity; + this.adapter = adapter; + } + + @Override + public void run() { + try { + SearchEngine engine = + ServiceList.getService(serviceId).getSearchEngineInstance(new Downloader()); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(a); + String searchLanguageKey = a.getString(R.string.search_language_key); + String searchLanguage = sp.getString(searchLanguageKey, + a.getString(R.string.default_language_value)); + List suggestions = engine.suggestionList(query,searchLanguage,new Downloader()); + h.post(new SuggestionResultRunnable(suggestions, adapter)); + } catch (ExtractionException e) { + ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + ServiceList.getNameOfService(serviceId), query, R.string.parsing_error)); + e.printStackTrace(); + } catch (IOException e) { + postNewErrorToast(h, R.string.network_error); + e.printStackTrace(); + } catch (Exception e) { + ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + ServiceList.getNameOfService(serviceId), query, R.string.general_error)); + } + } + + private void postNewErrorToast(Handler h, final int stringResource) { + h.post(new Runnable() { + @Override + public void run() { + Toast.makeText(a, a.getString(stringResource), + Toast.LENGTH_SHORT).show(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/NewPipeSettings.java rename to app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 6a8cd7e84..0c3d29e59 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -18,7 +18,7 @@ * along with NewPipe. If not, see . */ -package org.schabi.newpipe; +package org.schabi.newpipe.settings; import android.content.Context; import android.content.SharedPreferences; @@ -26,6 +26,8 @@ import android.os.Environment; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import org.schabi.newpipe.R; + import java.io.File; import us.shandian.giga.util.Utility; diff --git a/app/src/main/java/org/schabi/newpipe/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/SettingsActivity.java rename to app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java index 06f3a213d..e62f90c45 100644 --- a/app/src/main/java/org/schabi/newpipe/SettingsActivity.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.settings; import android.content.Context; import android.content.Intent; @@ -14,6 +14,8 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import org.schabi.newpipe.R; + /** * Created by Christian Schabesberger on 31.08.15. diff --git a/app/src/main/java/org/schabi/newpipe/SettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java similarity index 99% rename from app/src/main/java/org/schabi/newpipe/SettingsFragment.java rename to app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java index be44e164a..401cef551 100644 --- a/app/src/main/java/org/schabi/newpipe/SettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsFragment.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.settings; import android.app.Activity; import android.content.ClipData; @@ -16,6 +16,9 @@ import android.preference.PreferenceScreen; import com.nononsenseapps.filepicker.FilePickerActivity; +import org.schabi.newpipe.App; +import org.schabi.newpipe.R; + import java.util.ArrayList; import info.guardianproject.netcipher.proxy.OrbotHelper; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java index 2dc123ce8..558602ca3 100755 --- a/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadManagerImpl.java @@ -5,7 +5,7 @@ import android.util.Log; import com.google.gson.Gson; -import org.schabi.newpipe.NewPipeSettings; +import org.schabi.newpipe.settings.NewPipeSettings; import java.io.File; import java.io.RandomAccessFile; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index f28c2d19f..ca85ae727 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -14,7 +14,7 @@ import android.os.Message; import android.support.v4.app.NotificationCompat.Builder; import android.util.Log; -import org.schabi.newpipe.NewPipeSettings; +import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.R; import us.shandian.giga.get.DownloadManager; import us.shandian.giga.get.DownloadManagerImpl; diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index cdec3e03d..d38958b4b 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -17,9 +17,8 @@ import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import org.schabi.newpipe.NewPipeSettings; +import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.R; -import us.shandian.giga.get.DownloadMission; import com.nononsenseapps.filepicker.FilePickerActivity; import com.nononsenseapps.filepicker.AbstractFilePickerFragment; diff --git a/app/src/main/res/drawable-hdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_rss_feed_white_24dp.png new file mode 100644 index 000000000..c966a6e0a Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_rss_feed_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_rss_feed_white_24dp.png new file mode 100644 index 000000000..067b10752 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_rss_feed_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_rss_feed_white_24dp.png new file mode 100644 index 000000000..650627286 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rss_feed_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_rss_feed_white_24dp.png new file mode 100644 index 000000000..2267aad8f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_rss_feed_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_white_24dp.png new file mode 100644 index 000000000..209688dc5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_rss_feed_white_24dp.png differ diff --git a/app/src/main/res/drawable/white_circle.xml b/app/src/main/res/drawable/white_circle.xml new file mode 100644 index 000000000..312f9c37c --- /dev/null +++ b/app/src/main/res/drawable/white_circle.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/app/src/main/res/layout-sw600dp-land/activity_videoitem_list.xml b/app/src/main/res/layout-sw600dp-land/activity_videoitem_list.xml deleted file mode 100644 index 4704bd3d2..000000000 --- a/app/src/main/res/layout-sw600dp-land/activity_videoitem_list.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-v18/fragment_videoitem_detail.xml b/app/src/main/res/layout-v18/fragment_videoitem_detail.xml index 8473c098c..0087bb749 100644 --- a/app/src/main/res/layout-v18/fragment_videoitem_detail.xml +++ b/app/src/main/res/layout-v18/fragment_videoitem_detail.xml @@ -3,15 +3,15 @@ + android:id="@+id/video_item_detail"> - - -