From 746c2a15bff4c1cba2dcca7a25813e692e0c1a53 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sun, 9 Apr 2017 14:34:00 -0300 Subject: [PATCH 1/3] Migrate to fragments and improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrate to fragments - Fix #487 - Don't show "Open in popup mode" to channel links - New backstack of videos - Change the subscribers count to format using `NumberFormat`, for example some locales use `.`  others `,`, this handles it automatically (and the old method had a bug for leading zero, e.g. 4.82.125 instead of 4.082.125) - Add string 'subscribers' for channels with more than 1 subscriber (plural) - Popup player chooses the default format and resolution based on the new preference (format) - Fix taskaffinity of the router activities - Show title before loading, as it is available from the items already loaded --- app/src/main/AndroidManifest.xml | 31 +- .../org/schabi/newpipe/ChannelActivity.java | 407 ------------- .../newpipe/ImageErrorLoadingListener.java | 12 +- .../java/org/schabi/newpipe/MainActivity.java | 210 ++++++- .../org/schabi/newpipe/RouterActivity.java | 76 +-- ...Activity.java => RouterPopupActivity.java} | 92 ++- .../newpipe/detail/StreamExtractorWorker.java | 250 -------- .../fragments/OnItemSelectedListener.java | 10 + .../fragments/channel/ChannelFragment.java | 416 ++++++++++++++ .../detail/ActionBarHandler.java | 57 +- .../newpipe/fragments/detail/StackItem.java | 31 + .../detail/VideoDetailFragment.java} | 535 ++++++++++++------ .../search/SearchFragment.java} | 353 +++++++----- .../search}/SearchSuggestionListener.java | 2 +- .../search}/SearchWorker.java | 7 +- .../search}/SuggestionListAdapter.java | 2 +- .../search}/SuggestionSearchRunnable.java | 10 +- .../newpipe/info_list/InfoItemBuilder.java | 65 ++- .../schabi/newpipe/player/AbstractPlayer.java | 13 +- .../newpipe/player/BackgroundPlayer.java | 14 +- .../newpipe/player/ExoPlayerActivity.java | 14 +- .../newpipe/player/PopupVideoPlayer.java | 69 +-- .../org/schabi/newpipe/util/Constants.java | 8 + .../org/schabi/newpipe/util/NavStack.java | 148 ----- .../schabi/newpipe/util/NavigationHelper.java | 96 ++++ .../java/org/schabi/newpipe/util/Utils.java | 91 +++ .../workers/ChannelExtractorWorker.java | 102 ++++ .../newpipe/workers/ExtractorWorker.java | 168 ++++++ .../workers/StreamExtractorWorker.java | 136 +++++ app/src/main/res/layout/activity_main.xml | 13 +- ...ivity_channel.xml => fragment_channel.xml} | 22 +- ...searchinfoitem.xml => fragment_search.xml} | 7 +- ...m_detail.xml => fragment_video_detail.xml} | 31 +- app/src/main/res/layout/main_bg.xml | 2 +- app/src/main/res/menu/main_menu.xml | 10 +- app/src/main/res/menu/menu_channel.xml | 8 +- app/src/main/res/menu/search_menu.xml | 3 +- app/src/main/res/menu/videoitem_detail.xml | 10 - app/src/main/res/values/strings.xml | 1 + 39 files changed, 2026 insertions(+), 1506 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/ChannelActivity.java rename app/src/main/java/org/schabi/newpipe/{PopupActivity.java => RouterPopupActivity.java} (79%) delete mode 100644 app/src/main/java/org/schabi/newpipe/detail/StreamExtractorWorker.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/OnItemSelectedListener.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java rename app/src/main/java/org/schabi/newpipe/{ => fragments}/detail/ActionBarHandler.java (77%) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java rename app/src/main/java/org/schabi/newpipe/{detail/VideoItemDetailActivity.java => fragments/detail/VideoDetailFragment.java} (63%) rename app/src/main/java/org/schabi/newpipe/{search_fragment/SearchInfoItemFragment.java => fragments/search/SearchFragment.java} (54%) rename app/src/main/java/org/schabi/newpipe/{search_fragment => fragments/search}/SearchSuggestionListener.java (97%) rename app/src/main/java/org/schabi/newpipe/{search_fragment => fragments/search}/SearchWorker.java (99%) rename app/src/main/java/org/schabi/newpipe/{search_fragment => fragments/search}/SuggestionListAdapter.java (98%) rename app/src/main/java/org/schabi/newpipe/{search_fragment => fragments/search}/SuggestionSearchRunnable.java (98%) create mode 100644 app/src/main/java/org/schabi/newpipe/util/Constants.java delete mode 100644 app/src/main/java/org/schabi/newpipe/util/NavStack.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/Utils.java create mode 100644 app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java create mode 100644 app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java create mode 100644 app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java rename app/src/main/res/layout/{activity_channel.xml => fragment_channel.xml} (50%) rename app/src/main/res/layout/{fragment_searchinfoitem.xml => fragment_search.xml} (83%) rename app/src/main/res/layout/{activity_videoitem_detail.xml => fragment_video_detail.xml} (96%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0cf26b394..e3f70937b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,22 +19,15 @@ tools:ignore="AllowBackup"> + android:label="@string/app_name" + android:launchMode="singleTask"> - - - + - @@ -104,7 +94,9 @@ android:resource="@xml/provider_paths" /> - @@ -161,7 +153,9 @@ - @@ -182,9 +176,6 @@ - - - @@ -212,9 +203,7 @@ - - diff --git a/app/src/main/java/org/schabi/newpipe/ChannelActivity.java b/app/src/main/java/org/schabi/newpipe/ChannelActivity.java deleted file mode 100644 index efc1e29f7..000000000 --- a/app/src/main/java/org/schabi/newpipe/ChannelActivity.java +++ /dev/null @@ -1,407 +0,0 @@ -package org.schabi.newpipe; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import com.nostra13.universalimageloader.core.ImageLoader; - -import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.channel.ChannelExtractor; -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.info_list.InfoItemBuilder; -import org.schabi.newpipe.info_list.InfoListAdapter; -import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.settings.SettingsActivity; -import org.schabi.newpipe.util.NavStack; -import org.schabi.newpipe.util.ThemeHelper; - -import java.io.IOException; - -import static android.os.Build.VERSION.SDK_INT; - - -/** - * Copyright (C) Christian Schabesberger 2016 - * ChannelActivity.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 ChannelActivity extends AppCompatActivity { - private static final String TAG = ChannelActivity.class.toString(); - private View rootView = null; - - 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; - - private String subS = ""; - - ProgressBar progressBar = null; - ImageView channelBanner = null; - ImageView avatarView = null; - TextView titleView = null; - TextView subscirberView = null; - Button subscriberButton = null; - View subscriberLayout = null; - - View header = null; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ThemeHelper.setTheme(this, true); - setContentView(R.layout.activity_channel); - rootView = findViewById(android.R.id.content); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowTitleEnabled(true); - - infoListAdapter = new InfoListAdapter(this, rootView); - RecyclerView recyclerView = (RecyclerView) findViewById(R.id.channel_streams_view); - final LinearLayoutManager layoutManager = new LinearLayoutManager(this); - recyclerView.setLayoutManager(layoutManager); - header = getLayoutInflater().inflate(R.layout.channel_header, recyclerView, false); - infoListAdapter.setHeader(header); - infoListAdapter.setFooter( - getLayoutInflater().inflate(R.layout.pignate_footer, recyclerView, false)); - recyclerView.setAdapter(infoListAdapter); - infoListAdapter.setOnStreamInfoItemSelectedListener( - new InfoItemBuilder.OnInfoItemSelectedListener() { - @Override - public void selected(String url, int serviceId) { - NavStack.getInstance() - .openDetailActivity(ChannelActivity.this, url, serviceId); - } - }); - - // 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); - } - } - } - }); - - subS = getString(R.string.subscriber); - - progressBar = (ProgressBar) findViewById(R.id.progressBar); - channelBanner = (ImageView) header.findViewById(R.id.channel_banner_image); - avatarView = (ImageView) header.findViewById(R.id.channel_avatar_view); - titleView = (TextView) header.findViewById(R.id.channel_title_view); - subscirberView = (TextView) header.findViewById(R.id.channel_subscriber_view); - subscriberButton = (Button) header.findViewById(R.id.channel_subscribe_button); - subscriberLayout = header.findViewById(R.id.channel_subscriber_layout); - - if(savedInstanceState == null) { - handleIntent(getIntent()); - } else { - channelUrl = savedInstanceState.getString(NavStack.URL); - serviceId = savedInstanceState.getInt(NavStack.SERVICE_ID); - NavStack.getInstance() - .restoreSavedInstanceState(savedInstanceState); - handleIntent(getIntent()); - } - - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - handleIntent(intent); - } - - private void handleIntent(Intent i) { - channelUrl = i.getStringExtra(NavStack.URL); - serviceId = i.getIntExtra(NavStack.SERVICE_ID, -1); - requestData(false); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString(NavStack.URL, channelUrl); - outState.putInt(NavStack.SERVICE_ID, serviceId); - NavStack.getInstance() - .onSaveInstanceState(outState); - } - - private void updateUi(final ChannelInfo info) { - findViewById(R.id.channel_header_layout).setVisibility(View.VISIBLE); - progressBar.setVisibility(View.GONE); - - if(info.channel_name != null && !info.channel_name.isEmpty()) { - getSupportActionBar().setTitle(info.channel_name); - titleView.setText(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); - imageLoader.displayImage(info.avatar_url, avatarView, - new ImageErrorLoadingListener(this, rootView ,info.service_id)); - } - - if(info.subscriberCount != -1) { - subscirberView.setText(buildSubscriberString(info.subscriberCount)); - } - - if((info.feed_url != null && !info.feed_url.isEmpty()) || - (info.subscriberCount != -1)) { - subscriberLayout.setVisibility(View.VISIBLE); - } - - if(info.feed_url != null && !info.feed_url.isEmpty()) { - subscriberButton.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 { - subscriberButton.setVisibility(View.INVISIBLE); - } - - } - - private void addVideos(final ChannelInfo info) { - infoListAdapter.addInfoItemList(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; - - if(!onlyVideos) { - //delete already displayed content - progressBar.setVisibility(View.VISIBLE); - infoListAdapter.clearSteamItemList(); - pageNumber = 0; - subscriberLayout.setVisibility(View.GONE); - titleView.setText(""); - getSupportActionBar().setTitle(""); - if (SDK_INT >= 21) { - channelBanner.setImageDrawable(getDrawable(R.drawable.channel_banner)); - avatarView.setImageDrawable(getDrawable(R.drawable.buddy)); - } - infoListAdapter.showFooter(false); - } - - Thread channelExtractorThread = new Thread(new Runnable() { - Handler h = new Handler(); - - @Override - public void run() { - StreamingService service = null; - try { - service = NewPipe.getService(serviceId); - ChannelExtractor extractor = service.getChannelExtractorInstance( - channelUrl, pageNumber); - - final ChannelInfo info = ChannelInfo.getInfo(extractor); - - - h.post(new Runnable() { - @Override - public void run() { - isLoading = false; - if(!onlyVideos) { - updateUi(info); - infoListAdapter.showFooter(true); - } - hasNextPage = info.hasNextPage; - if(!hasNextPage) { - infoListAdapter.showFooter(false); - } - 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_CHANNEL, - 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, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, - service.getServiceInfo().name, channelUrl, R.string.parsing_error)); - h.post(new Runnable() { - @Override - public void run() { - ChannelActivity.this.finish(); - } - }); - pe.printStackTrace(); - } catch(ExtractionException ex) { - String name = "none"; - if(service != null) { - name = service.getServiceInfo().name; - } - ErrorActivity.reportError(h, ChannelActivity.this, ex, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, - 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, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, - service.getServiceInfo().name, channelUrl, R.string.general_error)); - h.post(new Runnable() { - @Override - public void run() { - ChannelActivity.this.finish(); - } - }); - e.printStackTrace(); - } - } - }); - - channelExtractorThread.start(); - } - - @Override - public void onBackPressed() { - try { - NavStack.getInstance() - .navBack(this); - } catch (Exception e) { - ErrorActivity.reportUiError(this, e); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.menu_channel, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - super.onOptionsItemSelected(item); - switch(item.getItemId()) { - case R.id.action_settings: { - Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - return true; - } - case R.id.menu_item_openInBrowser: { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(channelUrl)); - - startActivity(Intent.createChooser(intent, getString(R.string.choose_browser))); - } - case R.id.menu_item_share: - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, channelUrl); - intent.setType("text/plain"); - startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); - case android.R.id.home: - NavStack.getInstance().openMainActivity(this); - default: - return super.onOptionsItemSelected(item); - } - } - - private String buildSubscriberString(long count) { - String out = ""; - if(count >= 1000000000){ - out += Long.toString((count/1000000000)%1000)+"."; - } - if(count>=1000000){ - out += Long.toString((count/1000000)%1000) + "."; - } - if(count>=1000){ - out += Long.toString((count/1000)%1000)+"."; - } - out += Long.toString(count%1000) + " " + subS; - return out; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java b/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java index 837d96846..8f1940fda 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java +++ b/app/src/main/java/org/schabi/newpipe/ImageErrorLoadingListener.java @@ -1,14 +1,14 @@ package org.schabi.newpipe; -import android.app.Activity; +import android.content.Context; 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.report.ErrorActivity; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.report.ErrorActivity; /** * Created by Christian Schabesberger on 01.08.16. @@ -33,11 +33,11 @@ import org.schabi.newpipe.extractor.NewPipe; public class ImageErrorLoadingListener implements ImageLoadingListener { private int serviceId = -1; - private Activity activity = null; + private Context context = null; private View rootView = null; - public ImageErrorLoadingListener(Activity activity, View rootView, int serviceId) { - this.activity = activity; + public ImageErrorLoadingListener(Context context, View rootView, int serviceId) { + this.context = context; this.serviceId= serviceId; this.rootView = rootView; } @@ -47,7 +47,7 @@ public class ImageErrorLoadingListener implements ImageLoadingListener { @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - ErrorActivity.reportError(activity, + ErrorActivity.reportError(context, failReason.getCause(), null, rootView, ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE, NewPipe.getNameOfService(serviceId), imageUri, diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 781460f0a..b3be7ba6a 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -1,54 +1,95 @@ +/* + * Created by Christian Schabesberger on 02.08.16. + *

+ * Copyright (C) Christian Schabesberger 2016 + * DownloadActivity.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 . + */ + package org.schabi.newpipe; import android.content.Intent; import android.media.AudioManager; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.support.v4.app.NavUtils; +import android.support.v4.app.FragmentManager; import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; + +import com.nostra13.universalimageloader.core.ImageLoader; + import org.schabi.newpipe.download.DownloadActivity; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.fragments.OnItemSelectedListener; +import org.schabi.newpipe.fragments.channel.ChannelFragment; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; +import org.schabi.newpipe.fragments.search.SearchFragment; import org.schabi.newpipe.settings.SettingsActivity; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; -/** - * Created by Christian Schabesberger on 02.08.16. - * - * Copyright (C) Christian Schabesberger 2016 - * DownloadActivity.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 MainActivity extends AppCompatActivity { - private Fragment mainFragment = null; +public class MainActivity extends AppCompatActivity implements OnItemSelectedListener { private static final String TAG = MainActivity.class.toString(); + /*////////////////////////////////////////////////////////////////////////// + // Activity's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); ThemeHelper.setTheme(this, true); + super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setVolumeControlStream(AudioManager.STREAM_MUSIC); - mainFragment = getSupportFragmentManager() - .findFragmentById(R.id.search_fragment); + if (savedInstanceState == null) initFragments(); } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + @Override + public void onBackPressed() { + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); + if (fragment instanceof VideoDetailFragment) if (((VideoDetailFragment) fragment).onActivityBackPressed()) return; + + if (getSupportFragmentManager().getBackStackEntryCount() >= 2) { + getSupportFragmentManager().popBackStackImmediate(); + } else { + if (fragment instanceof SearchFragment) { + SearchFragment searchFragment = (SearchFragment) fragment; + if (!searchFragment.isMainBgVisible()) { + getSupportFragmentManager().beginTransaction().remove(fragment).commitNow(); + NavigationHelper.openMainActivity(this); + return; + } + } + finish(); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); @@ -63,9 +104,10 @@ public class MainActivity extends AppCompatActivity { 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); + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); + if (fragment instanceof VideoDetailFragment) ((VideoDetailFragment) fragment).clearHistory(); + + NavigationHelper.openMainActivity(this); return true; } case R.id.action_settings: { @@ -85,4 +127,112 @@ public class MainActivity extends AppCompatActivity { return super.onOptionsItemSelected(item); } } + + /*////////////////////////////////////////////////////////////////////////// + // Init + //////////////////////////////////////////////////////////////////////////*/ + + private void initFragments() { + if (getIntent() != null && getIntent().hasExtra(Constants.KEY_URL)) { + handleIntent(getIntent()); + } else openSearchFragment(); + } + + /*////////////////////////////////////////////////////////////////////////// + // OnItemSelectedListener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onItemSelected(StreamingService.LinkType linkType, int serviceId, String url, String name) { + switch (linkType) { + case STREAM: + openVideoDetailFragment(serviceId, url, name, false); + break; + case CHANNEL: + openChannelFragment(serviceId, url, name); + break; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + + private void handleIntent(Intent intent) { + if (intent.hasExtra(Constants.KEY_LINK_TYPE)) { + String url = intent.getStringExtra(Constants.KEY_URL); + int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0); + try { + switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) { + case STREAM: + handleVideoDetailIntent(serviceId, url, intent); + break; + case CHANNEL: + handleChannelIntent(serviceId, url, intent); + break; + case NONE: + throw new Exception("Url not known to service. service=" + Integer.toString(serviceId) + " url=" + url); + } + } catch (Exception e) { + e.printStackTrace(); + } + } else { + getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); + openSearchFragment(); + } + } + + private void openSearchFragment() { + ImageLoader.getInstance().clearMemoryCache(); + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out) + .replace(R.id.fragment_holder, new SearchFragment()) + .addToBackStack(null) + .commit(); + } + + private void openVideoDetailFragment(int serviceId, String url, String title, boolean autoPlay) { + ImageLoader.getInstance().clearMemoryCache(); + + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); + if (title == null) title = ""; + + if (fragment instanceof VideoDetailFragment && fragment.isVisible()) { + VideoDetailFragment detailFragment = (VideoDetailFragment) fragment; + detailFragment.setAutoplay(autoPlay); + detailFragment.selectAndLoadVideo(serviceId, url, title); + return; + } + + VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title); + instance.setAutoplay(autoPlay); + + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out) + .replace(R.id.fragment_holder, instance) + .addToBackStack(null) + .commit(); + } + + private void openChannelFragment(int serviceId, String url, String name) { + ImageLoader.getInstance().clearMemoryCache(); + if (name == null) name = ""; + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out, android.R.anim.fade_in, android.R.anim.fade_out) + .replace(R.id.fragment_holder, ChannelFragment.newInstance(serviceId, url, name)) + .addToBackStack(null) + .commit(); + } + + private void handleVideoDetailIntent(int serviceId, String url, Intent intent) { + boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false); + String title = intent.getStringExtra(Constants.KEY_TITLE); + openVideoDetailFragment(serviceId, url, title, autoPlay); + } + + private void handleChannelIntent(int serviceId, String url, Intent intent) { + String name = intent.getStringExtra(Constants.KEY_TITLE); + openChannelFragment(serviceId, url, name); + } + } diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index e9cddf16d..134a8b102 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -3,19 +3,14 @@ package org.schabi.newpipe; import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; import android.widget.Toast; -import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.NavigationHelper; import java.util.Collection; import java.util.HashSet; -/** +/* * Copyright (C) Christian Schabesberger 2017 * RouterActivity .java is part of NewPipe. * @@ -38,7 +33,7 @@ import java.util.HashSet; * to the part of the service which can handle the url. */ public class RouterActivity extends Activity { - private static final String TAG = RouterActivity.class.toString(); + //private static final String TAG = "RouterActivity" /** * Removes invisible separators (\p{Z}) and punctuation characters including @@ -54,6 +49,25 @@ public class RouterActivity extends Activity { finish(); } + private void handleIntent(Intent intent) { + String videoUrl = ""; + + // first gather data and find service + if (intent.getData() != null) { + // this means the video was called though another app + videoUrl = intent.getData().toString(); + } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { + //this means that vidoe was called through share menu + String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + videoUrl = getUris(extraText)[0]; + } + + try { + NavigationHelper.openByLink(this, videoUrl); + } catch (Exception e) { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + } + } private static String removeHeadingGibberish(final String input) { int start = 0; @@ -107,50 +121,4 @@ public class RouterActivity extends Activity { return result.toArray(new String[result.size()]); } - private void handleIntent(Intent intent) { - String videoUrl = ""; - StreamingService service = null; - - // first gather data and find service - if (intent.getData() != null) { - // this means the video was called though another app - videoUrl = intent.getData().toString(); - } else if(intent.getStringExtra(Intent.EXTRA_TEXT) != null) { - //this means that vidoe was called through share menu - String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); - videoUrl = getUris(extraText)[0]; - } - - service = NewPipe.getServiceByUrl(videoUrl); - if(service == null) { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG) - .show(); - return; - } else { - Intent callIntent = new Intent(); - switch(service.getLinkTypeByUrl(videoUrl)) { - case CHANNEL: - callIntent.setClass(this, ChannelActivity.class); - break; - case STREAM: - callIntent.setClass(this, VideoItemDetailActivity.class); - callIntent.putExtra(VideoItemDetailActivity.AUTO_PLAY, - PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean( - getString(R.string.autoplay_through_intent_key), false)); - break; - case PLAYLIST: - Log.e(TAG, "NOT YET DEFINED"); - break; - default: - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG) - .show(); - return; - } - - callIntent.putExtra(NavStack.URL, videoUrl); - callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId()); - startActivity(callIntent); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/PopupActivity.java b/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java similarity index 79% rename from app/src/main/java/org/schabi/newpipe/PopupActivity.java rename to app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java index f0f1c1ea8..1cfcd29f2 100644 --- a/app/src/main/java/org/schabi/newpipe/PopupActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterPopupActivity.java @@ -4,24 +4,22 @@ import android.app.Activity; import android.content.Intent; import android.os.Build; import android.os.Bundle; -import android.util.Log; import android.widget.Toast; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.player.PopupVideoPlayer; -import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.PermissionHelper; import java.util.Collection; import java.util.HashSet; /** - * This activity is thought to open video streams form an external app using the popup playser. + * This activity is thought to open video streams form an external app using the popup player. */ - -public class PopupActivity extends Activity { - private static final String TAG = RouterActivity.class.toString(); +public class RouterPopupActivity extends Activity { + //private static final String TAG = "RouterPopupActivity"; /** * Removes invisible separators (\p{Z}) and punctuation characters including @@ -38,6 +36,45 @@ public class PopupActivity extends Activity { } + private void handleIntent(Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && !PermissionHelper.checkSystemAlertWindowPermission(this)) { + Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); + return; + } + String videoUrl = ""; + StreamingService service; + + // first gather data and find service + if (intent.getData() != null) { + // this means the video was called though another app + videoUrl = intent.getData().toString(); + } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { + //this means that vidoe was called through share menu + String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); + videoUrl = getUris(extraText)[0]; + } + + service = NewPipe.getServiceByUrl(videoUrl); + if (service == null) { + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + return; + } + + Intent callIntent = new Intent(this, PopupVideoPlayer.class); + switch (service.getLinkTypeByUrl(videoUrl)) { + case STREAM: + break; + default: + Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + return; + } + + callIntent.putExtra(Constants.KEY_URL, videoUrl); + callIntent.putExtra(Constants.KEY_SERVICE_ID, service.getServiceId()); + startService(callIntent); + } + private static String removeHeadingGibberish(final String input) { int start = 0; for (int i = input.indexOf("://") - 1; i >= 0; i--) { @@ -90,47 +127,4 @@ public class PopupActivity extends Activity { return result.toArray(new String[result.size()]); } - private void handleIntent(Intent intent) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && !PermissionHelper.checkSystemAlertWindowPermission(this)) { - Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); - return; - } - String videoUrl = ""; - StreamingService service = null; - - // first gather data and find service - if (intent.getData() != null) { - // this means the video was called though another app - videoUrl = intent.getData().toString(); - } else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) { - //this means that vidoe was called through share menu - String extraText = intent.getStringExtra(Intent.EXTRA_TEXT); - videoUrl = getUris(extraText)[0]; - } - - service = NewPipe.getServiceByUrl(videoUrl); - if (service == null) { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG) - .show(); - return; - } else { - Intent callIntent = new Intent(); - switch (service.getLinkTypeByUrl(videoUrl)) { - case STREAM: - callIntent.setClass(this, PopupVideoPlayer.class); - break; - case PLAYLIST: - Log.e(TAG, "NOT YET DEFINED"); - break; - default: - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - return; - } - - callIntent.putExtra(NavStack.URL, videoUrl); - callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId()); - startService(callIntent); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/detail/StreamExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/detail/StreamExtractorWorker.java deleted file mode 100644 index 730b5c653..000000000 --- a/app/src/main/java/org/schabi/newpipe/detail/StreamExtractorWorker.java +++ /dev/null @@ -1,250 +0,0 @@ -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.R; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; -import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; -import org.schabi.newpipe.extractor.stream_info.StreamExtractor; -import org.schabi.newpipe.extractor.stream_info.StreamInfo; -import org.schabi.newpipe.report.ErrorActivity; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Extract {@link StreamInfo} with {@link StreamExtractor} from the given url of the given service - */ -@SuppressWarnings("WeakerAccess") -public class StreamExtractorWorker extends Thread { - private static final String TAG = "StreamExtractorWorker"; - - private Activity activity; - private final String videoUrl; - private final int serviceId; - private OnStreamInfoReceivedListener callback; - - private final AtomicBoolean isRunning = new AtomicBoolean(false); - private final Handler handler = new Handler(); - - - public interface OnStreamInfoReceivedListener { - void onReceive(StreamInfo info); - void onError(int messageId); - void onReCaptchaException(); - void onBlockedByGemaError(); - void onContentErrorWithMessage(int messageId); - void onContentError(); - } - - public StreamExtractorWorker(Activity activity, String videoUrl, int serviceId, OnStreamInfoReceivedListener callback) { - this.serviceId = serviceId; - this.videoUrl = videoUrl; - this.activity = activity; - this.callback = callback; - } - - /** - * Returns a new instance already started of {@link StreamExtractorWorker}.
- * The caller is responsible to check if {@link StreamExtractorWorker#isRunning()}, or {@link StreamExtractorWorker#cancel()} it - * - * @param serviceId id of the request service - * @param url videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k) - * @param activity activity for error reporting purposes - * @param callback listener that will be called-back when events occur (check {@link OnStreamInfoReceivedListener}) - * @return new instance already started of {@link StreamExtractorWorker} - */ - public static StreamExtractorWorker startExtractorThread(int serviceId, String url, Activity activity, OnStreamInfoReceivedListener callback) { - StreamExtractorWorker extractorThread = getExtractorThread(serviceId, url, activity, callback); - extractorThread.start(); - return extractorThread; - } - - /** - * Returns a new instance of {@link StreamExtractorWorker}.
- * The caller is responsible to check if {@link StreamExtractorWorker#isRunning()}, or {@link StreamExtractorWorker#cancel()} - * when it doesn't need it anymore - *

- * Note: this instance is not started yet - * - * @param serviceId id of the request service - * @param url videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k) - * @param activity activity for error reporting purposes - * @param callback listener that will be called-back when events occur (check {@link OnStreamInfoReceivedListener}) - * @return instance of {@link StreamExtractorWorker} - */ - public static StreamExtractorWorker getExtractorThread(int serviceId, String url, Activity activity, OnStreamInfoReceivedListener callback) { - return new StreamExtractorWorker(activity, url, serviceId, callback); - } - - @Override - //Just ignore the errors for now - @SuppressWarnings("ConstantConditions") - public void run() { - // TODO: Improve error checking - // and this method in general - - StreamInfo streamInfo = null; - StreamingService service; - try { - service = NewPipe.getService(serviceId); - } catch (Exception e) { - e.printStackTrace(); - ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - "", videoUrl, R.string.could_not_get_stream)); - return; - } - try { - isRunning.set(true); - StreamExtractor streamExtractor = service.getExtractorInstance(videoUrl); - streamInfo = StreamInfo.getVideoInfo(streamExtractor); - - final StreamInfo info = streamInfo; - if (callback != null) handler.post(new Runnable() { - @Override - public void run() { - callback.onReceive(info); - } - }); - isRunning.set(false); - // 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 = activity != null ? activity.findViewById(R.id.video_item_detail) : null; - ErrorActivity.reportError(handler, activity, - 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 (ReCaptchaException e) { - if (callback != null) handler.post(new Runnable() { - @Override - public void run() { - callback.onReCaptchaException(); - } - }); - } catch (IOException e) { - if (callback != null) handler.post(new Runnable() { - @Override - public void run() { - callback.onError(R.string.network_error); - } - }); - if (callback != null) e.printStackTrace(); - } catch (YoutubeStreamExtractor.DecryptException de) { - // custom service related exceptions - ErrorActivity.reportError(handler, activity, de, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.youtube_signature_decryption_error)); - handler.post(new Runnable() { - @Override - public void run() { - activity.finish(); - } - }); - de.printStackTrace(); - } catch (YoutubeStreamExtractor.GemaException ge) { - if (callback != null) handler.post(new Runnable() { - @Override - public void run() { - callback.onBlockedByGemaError(); - } - }); - } catch (YoutubeStreamExtractor.LiveStreamException e) { - if (callback != null) handler.post(new Runnable() { - @Override - public void run() { - callback.onContentErrorWithMessage(R.string.live_streams_not_supported); - } - }); - } - // ---------------------------------------- - catch (StreamExtractor.ContentNotAvailableException e) { - if (callback != null) handler.post(new Runnable() { - @Override - public void run() { - callback.onContentError(); - } - }); - e.printStackTrace(); - } catch (StreamInfo.StreamExctractException e) { - if (!streamInfo.errors.isEmpty()) { - // !!! if this case ever kicks in someone gets kicked out !!! - ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); - } else { - ErrorActivity.reportError(handler, activity, streamInfo.errors, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.could_not_get_stream)); - } - handler.post(new Runnable() { - @Override - public void run() { - activity.finish(); - } - }); - e.printStackTrace(); - } catch (ParsingException e) { - ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.parsing_error)); - handler.post(new Runnable() { - @Override - public void run() { - activity.finish(); - } - }); - e.printStackTrace(); - } catch (Exception e) { - ErrorActivity.reportError(handler, activity, e, VideoItemDetailActivity.class, null, - ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, - service.getServiceInfo().name, videoUrl, R.string.general_error)); - handler.post(new Runnable() { - @Override - public void run() { - activity.finish(); - } - }); - e.printStackTrace(); - } - } - - /** - * Return true if the extraction is not completed yet - * - * @return the value of the AtomicBoolean {@link #isRunning} - */ - public boolean isRunning() { - return isRunning.get(); - } - - /** - * Cancel this ExtractorThread, setting the callback to null, the AtomicBoolean {@link #isRunning} to false and interrupt this thread. - *

- * Note: Any I/O that is active in the moment that this method is called will be canceled and a Exception will be thrown, because of the {@link #interrupt()}.
- * This is useful when you don't want the resulting {@link StreamInfo} anymore, but don't want to waste bandwidth, otherwise it'd run till it receives the StreamInfo. - */ - public void cancel() { - this.callback = null; - this.isRunning.set(false); - this.interrupt(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/OnItemSelectedListener.java b/app/src/main/java/org/schabi/newpipe/fragments/OnItemSelectedListener.java new file mode 100644 index 000000000..d35c063da --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/OnItemSelectedListener.java @@ -0,0 +1,10 @@ +package org.schabi.newpipe.fragments; + +import org.schabi.newpipe.extractor.StreamingService; + +/** + * Interface for communication purposes between activity and fragment + */ +public interface OnItemSelectedListener { + void onItemSelected(StreamingService.LinkType linkType, int serviceId, String url, String name); +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java new file mode 100644 index 000000000..fd28a3d23 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java @@ -0,0 +1,416 @@ +package org.schabi.newpipe.fragments.channel; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +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.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.nostra13.universalimageloader.core.ImageLoader; + +import org.schabi.newpipe.ImageErrorLoadingListener; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.fragments.OnItemSelectedListener; +import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; +import org.schabi.newpipe.report.ErrorActivity; +import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.workers.ChannelExtractorWorker; + +import java.text.NumberFormat; + +import static android.os.Build.VERSION.SDK_INT; + + +/** + * Copyright (C) Christian Schabesberger 2016 + * ChannelFragment.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 ChannelFragment extends Fragment implements ChannelExtractorWorker.OnChannelInfoReceive { + private static final String TAG = "ChannelFragment"; + + private AppCompatActivity activity; + private OnItemSelectedListener onItemSelectedListener; + private InfoListAdapter infoListAdapter; + + private ChannelExtractorWorker currentExtractorWorker; + private ChannelInfo currentChannelInfo; + private int serviceId = -1; + private String channelName = ""; + private String channelUrl = ""; + + private boolean isLoading = false; + private int pageNumber = 0; + private boolean hasNextPage = true; + + private ImageLoader imageLoader = ImageLoader.getInstance(); + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private View rootView = null; + + private RecyclerView channelVideosList; + private LinearLayoutManager layoutManager; + private ProgressBar loadingProgressBar; + + private View headerRootLayout; + private ImageView headerChannelBanner; + private ImageView headerAvatarView; + private TextView headerTitleView; + private TextView headerSubscriberView; + private Button headerSubscriberButton; + private View headerSubscriberLayout; + + /*////////////////////////////////////////////////////////////////////////*/ + + public ChannelFragment() { + } + + public static ChannelFragment newInstance(int serviceId, String url, String name) { + ChannelFragment instance = newInstance(); + + Bundle bundle = new Bundle(); + bundle.putString(Constants.KEY_URL, url); + bundle.putString(Constants.KEY_TITLE, name); + bundle.putInt(Constants.KEY_SERVICE_ID, serviceId); + + instance.setArguments(bundle); + return instance; + } + + public static ChannelFragment newInstance() { + return new ChannelFragment(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Fragment's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + activity = ((AppCompatActivity) context); + onItemSelectedListener = ((OnItemSelectedListener) context); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + isLoading = false; + if (savedInstanceState != null) { + channelUrl = savedInstanceState.getString(Constants.KEY_URL); + channelName = savedInstanceState.getString(Constants.KEY_TITLE); + serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1); + } else { + try { + Bundle args = getArguments(); + if (args != null) { + channelUrl = args.getString(Constants.KEY_URL); + channelName = args.getString(Constants.KEY_TITLE); + serviceId = args.getInt(Constants.KEY_SERVICE_ID, 0); + } + } catch (Exception e) { + e.printStackTrace(); + ErrorActivity.reportError(getActivity(), e, null, + getActivity().findViewById(android.R.id.content), + ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, + NewPipe.getNameOfService(serviceId), + "", R.string.general_error)); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + rootView = inflater.inflate(R.layout.fragment_channel, container, false); + return rootView; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + loadingProgressBar = (ProgressBar) view.findViewById(R.id.loading_progress_bar); + channelVideosList = (RecyclerView) view.findViewById(R.id.channel_streams_view); + + infoListAdapter = new InfoListAdapter(activity, rootView); + layoutManager = new LinearLayoutManager(activity); + channelVideosList.setLayoutManager(layoutManager); + + headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, channelVideosList, false); + infoListAdapter.setHeader(headerRootLayout); + infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, channelVideosList, false)); + channelVideosList.setAdapter(infoListAdapter); + + headerChannelBanner = (ImageView) headerRootLayout.findViewById(R.id.channel_banner_image); + headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view); + headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view); + headerSubscriberView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view); + headerSubscriberButton = (Button) headerRootLayout.findViewById(R.id.channel_subscribe_button); + headerSubscriberLayout = headerRootLayout.findViewById(R.id.channel_subscriber_layout); + + initListeners(); + + isLoading = true; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + headerAvatarView.setImageBitmap(null); + headerChannelBanner.setImageBitmap(null); + channelVideosList.removeAllViews(); + + rootView = null; + channelVideosList = null; + layoutManager = null; + loadingProgressBar = null; + headerRootLayout = null; + headerChannelBanner = null; + headerAvatarView = null; + headerTitleView = null; + headerSubscriberView = null; + headerSubscriberButton = null; + headerSubscriberLayout = null; + } + + @Override + public void onResume() { + super.onResume(); + if (isLoading) { + requestData(false); + } + } + + @Override + public void onStop() { + super.onStop(); + if (currentExtractorWorker != null) currentExtractorWorker.cancel(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + imageLoader.clearMemoryCache(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(Constants.KEY_URL, channelUrl); + outState.putString(Constants.KEY_TITLE, channelName); + outState.putInt(Constants.KEY_SERVICE_ID, serviceId); + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu_channel, menu); + + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + supportActionBar.setDisplayHomeAsUpEnabled(true); + //noinspection deprecation + supportActionBar.setNavigationMode(0); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + switch (item.getItemId()) { + case R.id.menu_item_openInBrowser: { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(channelUrl)); + + startActivity(Intent.createChooser(intent, getString(R.string.choose_browser))); + return true; + } + case R.id.menu_item_share: + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, channelUrl); + intent.setType("text/plain"); + startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Init's + //////////////////////////////////////////////////////////////////////////*/ + + private void initListeners() { + infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + @Override + public void selected(int serviceId, String url, String title) { + NavigationHelper.openVideoDetail(onItemSelectedListener, serviceId, url, title); + } + }); + + // detect if list has ben scrolled to the bottom + channelVideosList.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + int pastVisiblesItems, visibleItemCount, totalItemCount; + super.onScrolled(recyclerView, dx, dy); + //check for scroll down + if (dy > 0) { + visibleItemCount = layoutManager.getChildCount(); + totalItemCount = layoutManager.getItemCount(); + pastVisiblesItems = layoutManager.findFirstVisibleItemPosition(); + + if ((visibleItemCount + pastVisiblesItems) >= totalItemCount && !currentExtractorWorker.isRunning() && hasNextPage) { + pageNumber++; + requestData(true); + } + } + } + }); + + headerSubscriberButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Log.d(TAG, currentChannelInfo.feed_url); + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(currentChannelInfo.feed_url)); + startActivity(i); + } + }); + } + + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private String buildSubscriberString(long count) { + String out = NumberFormat.getNumberInstance().format(count); + out += " " + getString(count > 1 ? R.string.subscriber_plural : R.string.subscriber); + return out; + } + + private void requestData(boolean onlyVideos) { + if (currentExtractorWorker != null && currentExtractorWorker.isRunning()) currentExtractorWorker.cancel(); + + isLoading = true; + if (!onlyVideos) { + //delete already displayed content + loadingProgressBar.setVisibility(View.VISIBLE); + infoListAdapter.clearSteamItemList(); + pageNumber = 0; + headerSubscriberLayout.setVisibility(View.GONE); + headerTitleView.setText(channelName != null ? channelName : ""); + if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(channelName != null ? channelName : ""); + if (SDK_INT >= 21) { + headerChannelBanner.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner)); + headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); + } + infoListAdapter.showFooter(false); + } + + currentExtractorWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, pageNumber, this); + currentExtractorWorker.setOnlyVideos(onlyVideos); + currentExtractorWorker.start(); + } + + private void addVideos(ChannelInfo info) { + infoListAdapter.addInfoItemList(info.related_streams); + } + + /*////////////////////////////////////////////////////////////////////////// + // OnChannelInfoReceiveListener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onReceive(ChannelInfo info) { + if (info == null || isRemoving() || !isVisible()) return; + + currentChannelInfo = info; + + if (!currentExtractorWorker.isOnlyVideos()) { + headerRootLayout.setVisibility(View.VISIBLE); + loadingProgressBar.setVisibility(View.GONE); + + if (info.channel_name != null && !info.channel_name.isEmpty()) { + if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.channel_name); + headerTitleView.setText(info.channel_name); + channelName = info.channel_name; + } else channelName = ""; + + if (info.banner_url != null && !info.banner_url.isEmpty()) { + imageLoader.displayImage(info.banner_url, headerChannelBanner, + new ImageErrorLoadingListener(activity, rootView, info.service_id)); + } + + if (info.avatar_url != null && !info.avatar_url.isEmpty()) { + headerAvatarView.setVisibility(View.VISIBLE); + imageLoader.displayImage(info.avatar_url, headerAvatarView, + new ImageErrorLoadingListener(activity, rootView, info.service_id)); + } + + if (info.subscriberCount != -1) { + headerSubscriberView.setText(buildSubscriberString(info.subscriberCount)); + } + + if ((info.feed_url != null && !info.feed_url.isEmpty()) || (info.subscriberCount != -1)) { + headerSubscriberLayout.setVisibility(View.VISIBLE); + } + + if (info.feed_url == null || info.feed_url.isEmpty()) { + headerSubscriberButton.setVisibility(View.INVISIBLE); + } + + infoListAdapter.showFooter(true); + } + hasNextPage = info.hasNextPage; + if (!hasNextPage) infoListAdapter.showFooter(false); + addVideos(info); + isLoading = false; + } + + @Override + public void onError(int messageId) { + Toast.makeText(activity, messageId, Toast.LENGTH_LONG).show(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java similarity index 77% rename from app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java rename to app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java index 5b3c68c2e..d60fac737 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/ActionBarHandler.java @@ -1,6 +1,5 @@ -package org.schabi.newpipe.detail; +package org.schabi.newpipe.fragments.detail; -import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.v7.app.ActionBar; @@ -12,9 +11,9 @@ 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.stream_info.VideoStream; +import org.schabi.newpipe.util.Utils; import java.util.List; @@ -50,7 +49,7 @@ class ActionBarHandler { private Menu menu; // Only callbacks are listed here, there are more actions which don't need a callback. - // those are edited directly. Typically VideoItemDetailFragment will implement those callbacks. + // those are edited directly. Typically VideoDetailFragment will implement those callbacks. private OnActionListener onShareListener; private OnActionListener onOpenInBrowserListener; private OnActionListener onOpenInPopupListener; @@ -89,7 +88,7 @@ class ActionBarHandler { VideoStream item = videoStreams.get(i); itemArray[i] = MediaFormat.getNameById(item.format) + " " + item.resolution; } - int defaultResolution = getDefaultResolution(videoStreams); + int defaultResolution = Utils.getPreferredResolution(activity, videoStreams); ArrayAdapter itemAdapter = new ArrayAdapter<>(activity.getBaseContext(), android.R.layout.simple_spinner_dropdown_item, itemArray); @@ -110,43 +109,6 @@ class ActionBarHandler { } } - - private int getDefaultResolution(final List videoStreams) { - if (defaultPreferences == null) - return 0; - - String defaultResolution = defaultPreferences - .getString(activity.getString(R.string.default_resolution_key), - activity.getString(R.string.default_resolution_value)); - - String preferedFormat = defaultPreferences - .getString(activity.getString(R.string.preferred_video_format_key), - activity.getString(R.string.preferred_video_format_default)); - - // first try to find the one with the right resolution - int selectedFormat = 0; - for (int i = 0; i < videoStreams.size(); i++) { - VideoStream item = videoStreams.get(i); - if (defaultResolution.equals(item.resolution)) { - selectedFormat = i; - } - } - - // than try to find the one with the right resolution and format - for (int i = 0; i < videoStreams.size(); i++) { - VideoStream item = videoStreams.get(i); - if (defaultResolution.equals(item.resolution) - && preferedFormat.equals(MediaFormat.getNameById(item.format))) { - selectedFormat = i; - } - } - - - // this is actually an error, - // but maybe there is really no stream fitting to the default value. - return selectedFormat; - } - public void setupMenu(Menu menu, MenuInflater inflater) { this.menu = menu; @@ -187,11 +149,6 @@ class ActionBarHandler { onDownloadListener.onActionSelected(selectedVideoStream); } return true; - case R.id.action_settings: { - Intent intent = new Intent(activity, SettingsActivity.class); - activity.startActivity(intent); - return true; - } case R.id.action_play_with_kodi: if(onPlayWithKodiListener != null) { onPlayWithKodiListener.onActionSelected(selectedVideoStream); @@ -202,12 +159,6 @@ class ActionBarHandler { onPlayAudioListener.onActionSelected(selectedVideoStream); } return true; - case R.id.menu_item_downloads: { - Intent intent = - new Intent(activity, org.schabi.newpipe.download.DownloadActivity.class); - activity.startActivity(intent); - return true; - } case R.id.menu_item_popup: { if(onOpenInPopupListener != null) { onOpenInPopupListener.onActionSelected(selectedVideoStream); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java new file mode 100644 index 000000000..e335be1e3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/StackItem.java @@ -0,0 +1,31 @@ +package org.schabi.newpipe.fragments.detail; + +import java.io.Serializable; + + +@SuppressWarnings("WeakerAccess") +public class StackItem implements Serializable { + private String title, url; + + public StackItem(String url, String title) { + this.title = title; + this.url = url; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + + public String getUrl() { + return url; + } + + @Override + public String toString() { + return getUrl() + " > " + getTitle(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailActivity.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java similarity index 63% rename from app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailActivity.java rename to app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 188479009..8e803ff87 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailActivity.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1,7 +1,9 @@ -package org.schabi.newpipe.detail; +package org.schabi.newpipe.fragments.detail; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -11,14 +13,18 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; +import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.text.method.LinkMovementMethod; import android.util.Log; import android.util.TypedValue; +import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -49,6 +55,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.stream_info.AudioStream; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.VideoStream; +import org.schabi.newpipe.fragments.OnItemSelectedListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.player.AbstractPlayer; import org.schabi.newpipe.player.BackgroundPlayer; @@ -56,30 +63,38 @@ import org.schabi.newpipe.player.ExoPlayerActivity; import org.schabi.newpipe.player.PlayVideoActivity; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; -import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.util.Utils; +import org.schabi.newpipe.workers.StreamExtractorWorker; +import java.io.Serializable; import java.util.ArrayList; +import java.util.Stack; import java.util.concurrent.atomic.AtomicBoolean; @SuppressWarnings("FieldCanBeLocal") -public class VideoItemDetailActivity extends AppCompatActivity implements StreamExtractorWorker.OnStreamInfoReceivedListener, SharedPreferences.OnSharedPreferenceChangeListener { +public class VideoDetailFragment extends Fragment implements StreamExtractorWorker.OnStreamInfoReceivedListener, SharedPreferences.OnSharedPreferenceChangeListener { + + private final String TAG = "VideoDetailFragment@" + Integer.toHexString(hashCode()); - private static final String TAG = "VideoItemDetailActivity"; private static final String KORE_PACKET = "org.xbmc.kore"; + private static final String SERVICE_ID_KEY = "service_id_key"; + private static final String VIDEO_URL_KEY = "video_url_key"; + private static final String VIDEO_TITLE_KEY = "video_title_key"; + private static final String STACK_KEY = "stack_key"; - /** - * The fragment argument representing the item ID that this fragment - * represents. - */ public static final String AUTO_PLAY = "auto_play"; + private AppCompatActivity activity; + private OnItemSelectedListener onItemSelectedListener; private ActionBarHandler actionBarHandler; private InfoItemBuilder infoItemBuilder = null; private StreamInfo currentStreamInfo = null; - private StreamExtractorWorker curExtractorThread; + private StreamExtractorWorker curExtractorWorker; + + private String videoTitle; private String videoUrl; private int serviceId = -1; @@ -89,9 +104,9 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream private boolean autoPlayEnabled; private boolean showRelatedStreams; - private ImageLoader imageLoader = ImageLoader.getInstance(); - private DisplayImageOptions displayImageOptions = - new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(true).build(); + private static final ImageLoader imageLoader = ImageLoader.getInstance(); + private static final DisplayImageOptions displayImageOptions = + new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(false).build(); private Bitmap streamThumbnail = null; /*////////////////////////////////////////////////////////////////////////// @@ -130,55 +145,141 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream private RelativeLayout relatedStreamRootLayout; private LinearLayout relatedStreamsView; + /*////////////////////////////////////////////////////////////////////////*/ + + public static VideoDetailFragment getInstance(int serviceId, String url) { + return getInstance(serviceId, url, ""); + } + + public static VideoDetailFragment getInstance(int serviceId, String videoUrl, String videoTitle) { + VideoDetailFragment instance = getInstance(); + instance.selectVideo(serviceId, videoUrl, videoTitle); + return instance; + } + + public static VideoDetailFragment getInstance() { + return new VideoDetailFragment(); + } /*////////////////////////////////////////////////////////////////////////// - // Activity's Lifecycle + // Fragment's Lifecycle //////////////////////////////////////////////////////////////////////////*/ + @Override + public void onAttach(Context context) { + super.onAttach(context); + activity = (AppCompatActivity) context; + onItemSelectedListener = (OnItemSelectedListener) context; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + videoTitle = savedInstanceState.getString(VIDEO_TITLE_KEY); + videoUrl = savedInstanceState.getString(VIDEO_URL_KEY); + serviceId = savedInstanceState.getInt(SERVICE_ID_KEY); + Serializable serializable = savedInstanceState.getSerializable(STACK_KEY); + if (serializable instanceof Stack) { + //noinspection unchecked + Stack list = (Stack) serializable; + stack.clear(); + stack.addAll(list); + } + } - showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(getString(R.string.show_next_video_key), true); - PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this); - - ThemeHelper.setTheme(this, true); - setContentView(R.layout.activity_videoitem_detail); - setVolumeControlStream(AudioManager.STREAM_MUSIC); - - if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true); - else Log.e(TAG, "Could not get SupportActionBar"); - - initViews(); - initListeners(); - handleIntent(getIntent()); + showRelatedStreams = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_next_video_key), true); + PreferenceManager.getDefaultSharedPreferences(activity).registerOnSharedPreferenceChangeListener(this); + activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); + isLoading.set(false); + setHasOptionsMenu(true); } @Override - protected void onResume() { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_video_detail, container, false); + } + + @Override + public void onViewCreated(View rootView, Bundle savedInstanceState) { + initViews(rootView); + initListeners(); + isLoading.set(true); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + thumbnailImageView.setImageBitmap(null); + relatedStreamsView.removeAllViews(); + + loadingProgressBar = null; + + parallaxScrollRootView = null; + contentRootLayout = null; + + thumbnailBackgroundButton = null; + thumbnailImageView = null; + thumbnailPlayButton = null; + + videoTitleRoot = null; + videoTitleTextView = null; + videoTitleToggleArrow = null; + videoCountView = null; + + videoDescriptionRootLayout = null; + videoUploadDateView = null; + videoDescriptionView = null; + + uploaderButton = null; + uploaderTextView = null; + uploaderThumb = null; + + thumbsUpTextView = null; + thumbsUpImageView = null; + thumbsDownTextView = null; + thumbsDownImageView = null; + thumbsDisabledTextView = null; + + nextStreamTitle = null; + relatedStreamRootLayout = null; + relatedStreamsView = null; + } + + @Override + public void onResume() { super.onResume(); // Currently only used for enable/disable related videos - // but can be extended for other live settings change + // but can be extended for other live settings changes if (needUpdate) { if (relatedStreamsView != null) initRelatedVideos(currentStreamInfo); needUpdate = false; } + + // Check if it was loading when the activity was stopped/paused, + // because when this happen, the curExtractorWorker is cancelled + if (isLoading.get()) selectAndLoadVideo(serviceId, videoUrl, videoTitle); } @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - handleIntent(intent); + public void onStop() { + super.onStop(); + if (curExtractorWorker != null && curExtractorWorker.isRunning()) curExtractorWorker.cancel(); } @Override - public void onBackPressed() { - try { - NavStack.getInstance().navBack(this); - } catch (Exception e) { - ErrorActivity.reportUiError(this, e); - } + public void onDestroy() { + super.onDestroy(); + PreferenceManager.getDefaultSharedPreferences(activity).unregisterOnSharedPreferenceChangeListener(this); + imageLoader.clearMemoryCache(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putString(VIDEO_URL_KEY, videoUrl); + outState.putString(VIDEO_TITLE_KEY, videoTitle); + outState.putInt(SERVICE_ID_KEY, serviceId); + outState.putSerializable(STACK_KEY, stack); } @Override @@ -186,9 +287,8 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case ReCaptchaActivity.RECAPTCHA_REQUEST: - if (resultCode == RESULT_OK) { - String videoUrl = getIntent().getStringExtra(NavStack.URL); - NavStack.getInstance().openDetailActivity(this, videoUrl, serviceId); + if (resultCode == Activity.RESULT_OK) { + NavigationHelper.openVideoDetail(onItemSelectedListener, serviceId, videoUrl, videoTitle); } else Log.e(TAG, "ReCaptcha failed"); break; default: @@ -209,48 +309,47 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream // Init //////////////////////////////////////////////////////////////////////////*/ - public void initViews() { - loadingProgressBar = (ProgressBar) findViewById(R.id.detail_loading_progress_bar); + private void initViews(View rootView) { + loadingProgressBar = (ProgressBar) rootView.findViewById(R.id.detail_loading_progress_bar); - parallaxScrollRootView = (ParallaxScrollView) findViewById(R.id.detail_main_content); + parallaxScrollRootView = (ParallaxScrollView) rootView.findViewById(R.id.detail_main_content); - //thumbnailRootLayout = (RelativeLayout) findViewById(R.id.detail_thumbnail_root_layout); - thumbnailBackgroundButton = (Button) findViewById(R.id.detail_stream_thumbnail_background_button); - thumbnailImageView = (ImageView) findViewById(R.id.detail_thumbnail_image_view); - thumbnailPlayButton = (ImageView) findViewById(R.id.detail_thumbnail_play_button); + //thumbnailRootLayout = (RelativeLayout) rootView.findViewById(R.id.detail_thumbnail_root_layout); + thumbnailBackgroundButton = (Button) rootView.findViewById(R.id.detail_stream_thumbnail_background_button); + thumbnailImageView = (ImageView) rootView.findViewById(R.id.detail_thumbnail_image_view); + thumbnailPlayButton = (ImageView) rootView.findViewById(R.id.detail_thumbnail_play_button); - contentRootLayout = (RelativeLayout) findViewById(R.id.detail_content_root_layout); + contentRootLayout = (RelativeLayout) rootView.findViewById(R.id.detail_content_root_layout); - videoTitleRoot = findViewById(R.id.detail_title_root_layout); - videoTitleTextView = (TextView) findViewById(R.id.detail_video_title_view); - videoTitleToggleArrow = (ImageView) findViewById(R.id.detail_toggle_description_view); - videoCountView = (TextView) findViewById(R.id.detail_view_count_view); + videoTitleRoot = rootView.findViewById(R.id.detail_title_root_layout); + videoTitleTextView = (TextView) rootView.findViewById(R.id.detail_video_title_view); + videoTitleToggleArrow = (ImageView) rootView.findViewById(R.id.detail_toggle_description_view); + videoCountView = (TextView) rootView.findViewById(R.id.detail_view_count_view); - videoDescriptionRootLayout = (RelativeLayout) findViewById(R.id.detail_description_root_layout); - videoUploadDateView = (TextView) findViewById(R.id.detail_upload_date_view); - videoDescriptionView = (TextView) findViewById(R.id.detail_description_view); + videoDescriptionRootLayout = (RelativeLayout) rootView.findViewById(R.id.detail_description_root_layout); + videoUploadDateView = (TextView) rootView.findViewById(R.id.detail_upload_date_view); + videoDescriptionView = (TextView) rootView.findViewById(R.id.detail_description_view); - //thumbsRootLayout = (LinearLayout) findViewById(R.id.detail_thumbs_root_layout); - thumbsUpTextView = (TextView) findViewById(R.id.detail_thumbs_up_count_view); - thumbsUpImageView = (ImageView) findViewById(R.id.detail_thumbs_up_img_view); - thumbsDownTextView = (TextView) findViewById(R.id.detail_thumbs_down_count_view); - thumbsDownImageView = (ImageView) findViewById(R.id.detail_thumbs_down_img_view); - thumbsDisabledTextView = (TextView) findViewById(R.id.detail_thumbs_disabled_view); + //thumbsRootLayout = (LinearLayout) rootView.findViewById(R.id.detail_thumbs_root_layout); + thumbsUpTextView = (TextView) rootView.findViewById(R.id.detail_thumbs_up_count_view); + thumbsUpImageView = (ImageView) rootView.findViewById(R.id.detail_thumbs_up_img_view); + thumbsDownTextView = (TextView) rootView.findViewById(R.id.detail_thumbs_down_count_view); + thumbsDownImageView = (ImageView) rootView.findViewById(R.id.detail_thumbs_down_img_view); + thumbsDisabledTextView = (TextView) rootView.findViewById(R.id.detail_thumbs_disabled_view); - //uploaderRootLayout = (FrameLayout) findViewById(R.id.detail_uploader_root_layout); - uploaderButton = (Button) findViewById(R.id.detail_uploader_button); - uploaderTextView = (TextView) findViewById(R.id.detail_uploader_text_view); - uploaderThumb = (ImageView) findViewById(R.id.detail_uploader_thumbnail_view); + //uploaderRootLayout = (FrameLayout) rootView.findViewById(R.id.detail_uploader_root_layout); + uploaderButton = (Button) rootView.findViewById(R.id.detail_uploader_button); + uploaderTextView = (TextView) rootView.findViewById(R.id.detail_uploader_text_view); + uploaderThumb = (ImageView) rootView.findViewById(R.id.detail_uploader_thumbnail_view); - relatedStreamRootLayout = (RelativeLayout) findViewById(R.id.detail_related_streams_root_layout); - nextStreamTitle = (TextView) findViewById(R.id.detail_next_stream_title); - relatedStreamsView = (LinearLayout) findViewById(R.id.detail_related_streams_view); + relatedStreamRootLayout = (RelativeLayout) rootView.findViewById(R.id.detail_related_streams_root_layout); + nextStreamTitle = (TextView) rootView.findViewById(R.id.detail_next_stream_title); + relatedStreamsView = (LinearLayout) rootView.findViewById(R.id.detail_related_streams_view); - actionBarHandler = new ActionBarHandler(this); - actionBarHandler.setupNavMenu(this); + actionBarHandler = new ActionBarHandler(activity); videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance()); - infoItemBuilder = new InfoItemBuilder(this, findViewById(android.R.id.content)); + infoItemBuilder = new InfoItemBuilder(activity, rootView.findViewById(android.R.id.content)); setHeightThumbnail(); } @@ -279,15 +378,16 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream infoItemBuilder.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override - public void selected(String url, int serviceId) { - NavStack.getInstance().openDetailActivity(VideoItemDetailActivity.this, url, serviceId); + public void selected(int serviceId, String url, String title) { + //NavigationHelper.openVideoDetail(activity, url, serviceId); + selectAndLoadVideo(serviceId, url, title); } }); uploaderButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - NavStack.getInstance().openChannelActivity(VideoItemDetailActivity.this, currentStreamInfo.channel_url, currentStreamInfo.service_id); + NavigationHelper.openChannel(onItemSelectedListener, currentStreamInfo.service_id, currentStreamInfo.channel_url, currentStreamInfo.uploader); } }); } @@ -311,14 +411,14 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; Intent intent = new Intent(AbstractPlayer.ACTION_UPDATE_THUMB); intent.putExtra(AbstractPlayer.VIDEO_URL, currentStreamInfo.webpage_url); - sendBroadcast(intent); + activity.sendBroadcast(intent); } } @Override public void onLoadingFailed(String imageUri, View view, FailReason failReason) { - ErrorActivity.reportError(VideoItemDetailActivity.this, - failReason.getCause(), null, findViewById(android.R.id.content), + ErrorActivity.reportError(activity, + failReason.getCause(), null, activity.findViewById(android.R.id.content), ErrorActivity.ErrorInfo.make(ErrorActivity.LOAD_IMAGE, NewPipe.getNameOfService(currentStreamInfo.service_id), imageUri, R.string.could_not_load_thumbnails)); @@ -330,7 +430,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream if (info.uploader_thumbnail_url != null && !info.uploader_thumbnail_url.isEmpty()) { imageLoader.displayImage(info.uploader_thumbnail_url, uploaderThumb, displayImageOptions, - new ImageErrorLoadingListener(this, findViewById(android.R.id.content), info.service_id)); + new ImageErrorLoadingListener(activity, activity.findViewById(android.R.id.content), info.service_id)); } } @@ -341,15 +441,15 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream nextStreamTitle.setVisibility(View.VISIBLE); relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, info.next_video)); relatedStreamsView.addView(getSeparatorView()); - relatedStreamsView.setVisibility(View.VISIBLE); + relatedStreamRootLayout.setVisibility(View.VISIBLE); } else nextStreamTitle.setVisibility(View.GONE); if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) { for (InfoItem item : info.related_streams) { relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); } - relatedStreamsView.setVisibility(View.VISIBLE); - } else if (info.next_video == null) relatedStreamsView.setVisibility(View.GONE); + relatedStreamRootLayout.setVisibility(View.VISIBLE); + } else if (info.next_video == null) relatedStreamRootLayout.setVisibility(View.GONE); } /*////////////////////////////////////////////////////////////////////////// @@ -357,21 +457,29 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream //////////////////////////////////////////////////////////////////////////*/ @Override - public boolean onCreateOptionsMenu(Menu menu) { - actionBarHandler.setupMenu(menu, getMenuInflater()); - return super.onCreateOptionsMenu(menu); + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + actionBarHandler.setupMenu(menu, inflater); + actionBarHandler.setupNavMenu(activity); + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayHomeAsUpEnabled(true); + supportActionBar.setDisplayShowTitleEnabled(false); + //noinspection deprecation + supportActionBar.setNavigationMode(0); + } } @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - NavStack.getInstance().openMainActivity(this); - return true; - } return actionBarHandler.onItemSelected(item) || super.onOptionsItemSelected(item); } private void setupActionBarHandler(final StreamInfo info) { + if (activity.getSupportActionBar() != null) { + //noinspection deprecation + activity.getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); + } + actionBarHandler.setupStreamList(info.video_streams); actionBarHandler.setOnShareListener(new ActionBarHandler.OnActionListener() { @Override @@ -382,7 +490,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream intent.setAction(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, info.webpage_url); intent.setType("text/plain"); - startActivity(Intent.createChooser(intent, VideoItemDetailActivity.this.getString(R.string.share_dialog_title))); + startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title))); } }); @@ -394,7 +502,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); intent.setData(Uri.parse(info.webpage_url)); - startActivity(Intent.createChooser(intent, VideoItemDetailActivity.this.getString(R.string.choose_browser))); + startActivity(Intent.createChooser(intent, activity.getString(R.string.choose_browser))); } }); @@ -403,21 +511,21 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream public void onActionSelected(int selectedStreamId) { if (isLoading.get()) return; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(VideoItemDetailActivity.this)) { - Toast.makeText(VideoItemDetailActivity.this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !PermissionHelper.checkSystemAlertWindowPermission(activity)) { + Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG).show(); return; } if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - Intent i = new Intent(VideoItemDetailActivity.this, PopupVideoPlayer.class); - Toast.makeText(VideoItemDetailActivity.this, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); + Intent i = new Intent(activity, PopupVideoPlayer.class); + Toast.makeText(activity, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); i.putExtra(AbstractPlayer.VIDEO_TITLE, info.title) .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) .putExtra(AbstractPlayer.INDEX_SEL_VIDEO_STREAM, selectedStreamId) .putExtra(AbstractPlayer.VIDEO_STREAMS_LIST, new ArrayList<>(info.video_streams)); if (info.start_position > 0) i.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); - VideoItemDetailActivity.this.startService(i); + activity.startService(i); } }); @@ -430,18 +538,18 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream Intent intent = new Intent(Intent.ACTION_VIEW); intent.setPackage(KORE_PACKET); intent.setData(Uri.parse(info.webpage_url.replace("https", "http"))); - VideoItemDetailActivity.this.startActivity(intent); + activity.startActivity(intent); } catch (Exception e) { e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(VideoItemDetailActivity.this); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setMessage(R.string.kore_not_found) .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(VideoItemDetailActivity.this.getString(R.string.fdroid_kore_url))); - VideoItemDetailActivity.this.startActivity(intent); + intent.setData(Uri.parse(activity.getString(R.string.fdroid_kore_url))); + activity.startActivity(intent); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @@ -459,7 +567,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream @Override public void onActionSelected(int selectedStreamId) { - if (isLoading.get() || !PermissionHelper.checkStoragePermissions(VideoItemDetailActivity.this)) { + if (isLoading.get() || !PermissionHelper.checkStoragePermissions(activity)) { return; } @@ -471,7 +579,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream if (info.audio_streams != null) { AudioStream audioStream = - info.audio_streams.get(getPreferredAudioStreamId(info)); + info.audio_streams.get(Utils.getPreferredAudioFormat(activity, info.audio_streams)); String audioSuffix = "." + MediaFormat.getSuffixById(audioStream.format); args.putString(DownloadDialog.AUDIO_URL, audioStream.url); @@ -487,9 +595,9 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream args.putString(DownloadDialog.TITLE, info.title); DownloadDialog downloadDialog = DownloadDialog.newInstance(args); - downloadDialog.show(VideoItemDetailActivity.this.getSupportFragmentManager(), "downloadDialog"); + downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); } catch (Exception e) { - Toast.makeText(VideoItemDetailActivity.this, + Toast.makeText(activity, R.string.could_not_setup_download_menu, Toast.LENGTH_LONG).show(); e.printStackTrace(); } @@ -504,17 +612,17 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream public void onActionSelected(int selectedStreamId) { if (isLoading.get()) return; - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(VideoItemDetailActivity.this) - .getBoolean(VideoItemDetailActivity.this.getString(R.string.use_external_audio_player_key), false); + boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); Intent intent; AudioStream audioStream = - info.audio_streams.get(getPreferredAudioStreamId(info)); + info.audio_streams.get(Utils.getPreferredAudioFormat(activity, info.audio_streams)); if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) { //internal music player: explicit intent if (!BackgroundPlayer.isRunning && streamThumbnail != null) { ActivityCommunicator.getCommunicator() .backgroundPlayerThumbnail = streamThumbnail; - intent = new Intent(VideoItemDetailActivity.this, BackgroundPlayer.class); + intent = new Intent(activity, BackgroundPlayer.class); intent.setAction(Intent.ACTION_VIEW); intent.setDataAndType(Uri.parse(audioStream.url), @@ -523,7 +631,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream intent.putExtra(BackgroundPlayer.WEB_URL, info.webpage_url); intent.putExtra(BackgroundPlayer.SERVICE_ID, serviceId); intent.putExtra(BackgroundPlayer.CHANNEL_NAME, info.uploader); - VideoItemDetailActivity.this.startService(intent); + activity.startService(intent); } } else { intent = new Intent(); @@ -534,18 +642,18 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream intent.putExtra(Intent.EXTRA_TITLE, info.title); intent.putExtra("title", info.title); // HERE !!! - VideoItemDetailActivity.this.startActivity(intent); + activity.startActivity(intent); } catch (Exception e) { e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(VideoItemDetailActivity.this); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setMessage(R.string.no_player_found) .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(VideoItemDetailActivity.this.getString(R.string.fdroid_vlc_url))); - VideoItemDetailActivity.this.startActivity(intent); + intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); + activity.startActivity(intent); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @@ -564,32 +672,109 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream } } + /*////////////////////////////////////////////////////////////////////////// + // OwnStack + //////////////////////////////////////////////////////////////////////////*/ + + /** + * Stack that contains the "navigation history".
+ * The peek is the current video. + */ + private final Stack stack = new Stack<>(); + + public void clearHistory() { + stack.clear(); + } + + public void pushToStack(String videoUrl, String videoTitle) { + + if (stack.size() > 0 && stack.peek().getUrl().equals(videoUrl)) return; + stack.push(new StackItem(videoUrl, videoTitle)); + + } + + public void setTitleToUrl(String videoUrl, String videoTitle) { + if (videoTitle != null && !videoTitle.isEmpty()) { + for (StackItem stackItem : stack) { + if (stackItem.getUrl().equals(videoUrl)) stackItem.setTitle(videoTitle); + } + } + } + + public boolean onActivityBackPressed() { + // That means that we are on the start of the stack, + // return false to let the MainActivity handle the onBack + if (stack.size() == 1) return false; + // Remove top + stack.pop(); + // Get url from the new top + StackItem peek = stack.peek(); + selectAndLoadVideo(0, peek.getUrl(), + peek.getTitle() != null && !peek.getTitle().isEmpty() ? peek.getTitle() : "" + ); + return true; + } + + /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ - private void handleIntent(Intent intent) { - if (intent == null) return; - serviceId = intent.getIntExtra(NavStack.SERVICE_ID, 0); - videoUrl = intent.getStringExtra(NavStack.URL); - autoPlayEnabled = intent.getBooleanExtra(AUTO_PLAY, false); - selectVideo(videoUrl, serviceId); + public void setAutoplay(boolean autoplay) { + this.autoPlayEnabled = autoplay; } - private void selectVideo(String url, int serviceId) { - if (curExtractorThread != null && curExtractorThread.isRunning()) curExtractorThread.cancel(); + public void selectVideo(int serviceId, String videoUrl, String videoTitle) { + this.videoUrl = videoUrl; + this.videoTitle = videoTitle; + this.serviceId = serviceId; + } - animateView(contentRootLayout, false, 200, null); + public void selectAndLoadVideo(int serviceId, String videoUrl, String videoTitle) { + selectVideo(serviceId, videoUrl, videoTitle); + loadSelectedVideo(); + } - thumbnailPlayButton.setVisibility(View.GONE); + public void loadSelectedVideo() { + pushToStack(videoUrl, videoTitle); + + if (curExtractorWorker != null && curExtractorWorker.isRunning()) curExtractorWorker.cancel(); + + if (activity.getSupportActionBar() != null) { + //noinspection deprecation + activity.getSupportActionBar().setNavigationMode(0); + } + + animateView(contentRootLayout, false, 50, null); + + videoTitleTextView.setMaxLines(1); + int scrollY = parallaxScrollRootView.getScrollY(); + if (scrollY < 30) animateView(videoTitleTextView, false, 200, new Runnable() { + @Override + public void run() { + videoTitleTextView.setText(videoTitle != null ? videoTitle : ""); + animateView(videoTitleTextView, true, 400, null); + } + }); + else videoTitleTextView.setText(videoTitle != null ? videoTitle : ""); + //videoTitleTextView.setText(videoTitle != null ? videoTitle : ""); + videoDescriptionRootLayout.setVisibility(View.GONE); + videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + videoTitleToggleArrow.setVisibility(View.GONE); + videoTitleRoot.setClickable(false); + + //thumbnailPlayButton.setVisibility(View.GONE); + animateView(thumbnailPlayButton, false, 50, null); loadingProgressBar.setVisibility(View.VISIBLE); imageLoader.cancelDisplayTask(thumbnailImageView); imageLoader.cancelDisplayTask(uploaderThumb); - thumbnailImageView.setImageDrawable(null); + thumbnailImageView.setImageBitmap(null); + uploaderThumb.setImageBitmap(null); - curExtractorThread = StreamExtractorWorker.startExtractorThread(serviceId, url, this, this); + curExtractorWorker = new StreamExtractorWorker(activity, serviceId, videoUrl, this); + curExtractorWorker.start(); isLoading.set(true); } @@ -597,7 +782,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream // ----------- THE MAGIC MOMENT --------------- VideoStream selectedVideoStream = info.video_streams.get(actionBarHandler.getSelectedVideoStream()); - if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { + if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(this.getString(R.string.use_external_video_player_key), false)) { // External Player Intent intent = new Intent(); @@ -609,7 +794,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream this.startActivity(intent); } catch (Exception e) { e.printStackTrace(); - AlertDialog.Builder builder = new AlertDialog.Builder(this); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setMessage(R.string.no_player_found) .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { @Override @@ -629,14 +814,13 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream } } else { Intent intent; - boolean useOldPlayer = PreferenceManager - .getDefaultSharedPreferences(this) + boolean useOldPlayer = PreferenceManager.getDefaultSharedPreferences(activity) .getBoolean(getString(R.string.use_old_player_key), false) || (Build.VERSION.SDK_INT < 16); if (!useOldPlayer) { // ExoPlayer if (streamThumbnail != null) ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail; - intent = new Intent(this, ExoPlayerActivity.class) + intent = new Intent(activity, ExoPlayerActivity.class) .putExtra(AbstractPlayer.VIDEO_TITLE, info.title) .putExtra(AbstractPlayer.VIDEO_URL, info.webpage_url) .putExtra(AbstractPlayer.CHANNEL_NAME, info.uploader) @@ -645,46 +829,19 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream if (info.start_position > 0) intent.putExtra(AbstractPlayer.START_POSITION, info.start_position * 1000); } else { // Internal Player - intent = new Intent(this, PlayVideoActivity.class) + intent = new Intent(activity, PlayVideoActivity.class) .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) .putExtra(PlayVideoActivity.START_POSITION, info.start_position); } - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + //intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } } - private int getPreferredAudioStreamId(final StreamInfo info) { - String preferredFormatString = PreferenceManager.getDefaultSharedPreferences(this) - .getString(getString(R.string.default_audio_format_key), "webm"); - - int preferredFormat = MediaFormat.WEBMA.id; - switch (preferredFormatString) { - case "webm": - preferredFormat = MediaFormat.WEBMA.id; - break; - case "m4a": - preferredFormat = MediaFormat.M4A.id; - break; - default: - break; - } - - for (int i = 0; i < info.audio_streams.size(); i++) { - if (info.audio_streams.get(i).format == preferredFormat) { - return i; - } - } - - //todo: make this a proper error - Log.e(TAG, "FAILED to set audioStream value!"); - return 0; - } - private View getSeparatorView() { - View separator = new View(this); + View separator = new View(activity); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1); int m8 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()); int m5 = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics()); @@ -692,7 +849,7 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream separator.setLayoutParams(params); TypedValue typedValue = new TypedValue(); - getTheme().resolveAttribute(R.attr.separatorColor, typedValue, true); + activity.getTheme().resolveAttribute(R.attr.separatorColor, typedValue, true); separator.setBackgroundColor(typedValue.data); return separator; } @@ -762,11 +919,11 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream @Override public void onReceive(StreamInfo info) { - currentStreamInfo = info; + if (info == null || isRemoving() || !isVisible()) return; + currentStreamInfo = info; loadingProgressBar.setVisibility(View.GONE); - thumbnailPlayButton.setVisibility(View.VISIBLE); - relatedStreamRootLayout.setVisibility(showRelatedStreams ? View.VISIBLE : View.GONE); + animateView(thumbnailPlayButton, true, 200, null); parallaxScrollRootView.scrollTo(0, 0); // Since newpipe is designed to work even if certain information is not available, @@ -775,9 +932,9 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream if (!info.uploader.isEmpty()) uploaderTextView.setText(info.uploader); uploaderTextView.setVisibility(!info.uploader.isEmpty() ? View.VISIBLE : View.GONE); uploaderButton.setVisibility(!info.channel_url.isEmpty() ? View.VISIBLE : View.GONE); - uploaderThumb.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.buddy)); + uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); - if (info.view_count >= 0) videoCountView.setText(Localization.localizeViewCount(info.view_count, this)); + if (info.view_count >= 0) videoCountView.setText(Localization.localizeViewCount(info.view_count, activity)); videoCountView.setVisibility(info.view_count >= 0 ? View.VISIBLE : View.GONE); if (info.dislike_count == -1 && info.like_count == -1) { @@ -790,54 +947,64 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream } else { thumbsDisabledTextView.setVisibility(View.GONE); - if (info.dislike_count >= 0) thumbsDownTextView.setText(Localization.localizeNumber(info.dislike_count, this)); + if (info.dislike_count >= 0) thumbsDownTextView.setText(Localization.localizeNumber(info.dislike_count, activity)); thumbsDownTextView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE); thumbsDownImageView.setVisibility(info.dislike_count >= 0 ? View.VISIBLE : View.GONE); - if (info.like_count >= 0) thumbsUpTextView.setText(Localization.localizeNumber(info.like_count, this)); + if (info.like_count >= 0) thumbsUpTextView.setText(Localization.localizeNumber(info.like_count, activity)); thumbsUpTextView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE); thumbsUpImageView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE); } - if (!info.upload_date.isEmpty()) videoUploadDateView.setText(Localization.localizeDate(info.upload_date, this)); + if (!info.upload_date.isEmpty()) videoUploadDateView.setText(Localization.localizeDate(info.upload_date, activity)); videoUploadDateView.setVisibility(!info.upload_date.isEmpty() ? View.VISIBLE : View.GONE); - if (!info.description.isEmpty()) videoDescriptionView.setText( - Build.VERSION.SDK_INT >= 24 ? Html.fromHtml(info.description, 0) : Html.fromHtml(info.description) - ); + if (!info.description.isEmpty()) { //noinspection deprecation + videoDescriptionView.setText(Build.VERSION.SDK_INT >= 24 ? Html.fromHtml(info.description, 0) : Html.fromHtml(info.description)); + } videoDescriptionView.setVisibility(!info.description.isEmpty() ? View.VISIBLE : View.GONE); videoDescriptionRootLayout.setVisibility(View.GONE); videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); + videoTitleToggleArrow.setVisibility(View.VISIBLE); + videoTitleRoot.setClickable(true); setupActionBarHandler(info); initRelatedVideos(info); initThumbnailViews(info); + setTitleToUrl(info.webpage_url, info.title); + animateView(contentRootLayout, true, 200, null); + if (autoPlayEnabled) { + playVideo(info); + // Only auto play in the first open + autoPlayEnabled = false; + } + isLoading.set(false); - if (autoPlayEnabled) playVideo(info); } @Override public void onError(int messageId) { - Toast.makeText(this, messageId, Toast.LENGTH_LONG).show(); + Toast.makeText(activity, messageId, Toast.LENGTH_LONG).show(); loadingProgressBar.setVisibility(View.GONE); - thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.not_available_monkey)); + videoTitleTextView.setText(getString(messageId)); + thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.not_available_monkey)); } @Override public void onReCaptchaException() { - Toast.makeText(this, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); + Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); // Starting ReCaptcha Challenge Activity - startActivityForResult(new Intent(this, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST); + startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST); } @Override public void onBlockedByGemaError() { loadingProgressBar.setVisibility(View.GONE); - thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.gruese_die_gema)); + thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.gruese_die_gema)); thumbnailBackgroundButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -848,20 +1015,20 @@ public class VideoItemDetailActivity extends AppCompatActivity implements Stream } }); - Toast.makeText(this, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); + Toast.makeText(activity, R.string.blocked_by_gema, Toast.LENGTH_LONG).show(); } @Override public void onContentErrorWithMessage(int messageId) { loadingProgressBar.setVisibility(View.GONE); - thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.not_available_monkey)); - Toast.makeText(this, messageId, Toast.LENGTH_LONG).show(); + thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.not_available_monkey)); + Toast.makeText(activity, messageId, Toast.LENGTH_LONG).show(); } @Override public void onContentError() { loadingProgressBar.setVisibility(View.GONE); - thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.not_available_monkey)); - Toast.makeText(this, R.string.content_not_available, Toast.LENGTH_LONG).show(); + thumbnailImageView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.not_available_monkey)); + Toast.makeText(activity, R.string.content_not_available, Toast.LENGTH_LONG).show(); } } diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java similarity index 54% rename from app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java rename to app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java index cb3869c01..e95473b2b 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchInfoItemFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java @@ -1,10 +1,13 @@ -package org.schabi.newpipe.search_fragment; +package org.schabi.newpipe.fragments.search; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; @@ -24,10 +27,11 @@ import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; +import org.schabi.newpipe.fragments.OnItemSelectedListener; import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.NavigationHelper; import java.util.EnumSet; @@ -36,120 +40,87 @@ import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST; /** * Created by Christian Schabesberger on 02.08.16. - * + *

* Copyright (C) Christian Schabesberger 2016 - * SearchInfoItemFragment.java is part of NewPipe. - * + * SearchFragment.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 SearchInfoItemFragment extends Fragment { +public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener, SearchWorker.SearchWorkerResultListener { - private static final String TAG = SearchInfoItemFragment.class.toString(); - - private EnumSet filter = - EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM); - - /** - * Listener for search queries - */ - public class SearchQueryListener implements SearchView.OnQueryTextListener { - - @Override - public boolean onQueryTextSubmit(String query) { - Activity a = getActivity(); - try { - 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, - NewPipe.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(); - } - 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 int pageNumber = 0; - private SuggestionListAdapter suggestionListAdapter = null; - private InfoListAdapter infoListAdapter = null; - private LinearLayoutManager streamInfoListLayoutManager = null; + private static final String TAG = SearchFragment.class.toString(); // savedInstanceBundle arguments private static final String QUERY = "query"; private static final String STREAMING_SERVICE = "streaming_service"; + private int streamingServiceId = -1; + private String searchQuery = ""; + private boolean isLoading = false; + + @SuppressWarnings("FieldCanBeLocal") + private SearchView searchView; + private RecyclerView recyclerView; + private ProgressBar loadingIndicator; + private int pageNumber = 0; + private SuggestionListAdapter suggestionListAdapter; + private InfoListAdapter infoListAdapter; + private LinearLayoutManager streamInfoListLayoutManager; + + private EnumSet filter = EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM); + private OnItemSelectedListener onItemSelectedListener; + /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). */ - public SearchInfoItemFragment() { + public SearchFragment() { } @SuppressWarnings("unused") - public static SearchInfoItemFragment newInstance(int streamingServiceId, String searchQuery) { + public static SearchFragment newInstance(int streamingServiceId, String searchQuery) { Bundle args = new Bundle(); args.putInt(STREAMING_SERVICE, streamingServiceId); args.putString(QUERY, searchQuery); - SearchInfoItemFragment fragment = new SearchInfoItemFragment(); + SearchFragment fragment = new SearchFragment(); fragment.setArguments(args); return fragment; } + /*////////////////////////////////////////////////////////////////////////// + // Fragment's LifeCycle + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + onItemSelectedListener = ((OnItemSelectedListener) context); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); searchQuery = ""; + isLoading = false; if (savedInstanceState != null) { searchQuery = savedInstanceState.getString(QUERY); streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE); } else { try { Bundle args = getArguments(); - if(args != null) { + if (args != null) { searchQuery = args.getString(QUERY); streamingServiceId = args.getInt(STREAMING_SERVICE); } else { @@ -168,50 +139,16 @@ public class SearchInfoItemFragment extends Fragment { setHasOptionsMenu(true); SearchWorker sw = SearchWorker.getInstance(); - sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() { - @Override - public void onResult(SearchResult result) { - infoListAdapter.addInfoItemList(result.resultList); - setDoneLoading(); - } - - @Override - public void onNothingFound(int stringResource) { - //setListShown(true); - Toast.makeText(getActivity(), getString(stringResource), - Toast.LENGTH_SHORT).show(); - setDoneLoading(); - } - - @Override - public void onError(String message) { - //setListShown(true); - Toast.makeText(getActivity(), message, - Toast.LENGTH_LONG).show(); - setDoneLoading(); - } - - @Override - public void onReCaptchaChallenge() { - Toast.makeText(getActivity(), "ReCaptcha Challenge requested", - Toast.LENGTH_LONG).show(); - - // Starting ReCaptcha Challenge Activity - startActivityForResult( - new Intent(getActivity(), ReCaptchaActivity.class), - RECAPTCHA_REQUEST); - } - }); + sw.setSearchWorkerResultListener(this); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_searchinfoitem, container, false); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_search, container, false); Context context = view.getContext(); - loadingIndicator = (ProgressBar) view.findViewById(R.id.progressBar); - RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list); + loadingIndicator = (ProgressBar) view.findViewById(R.id.loading_progress_bar); + recyclerView = (RecyclerView) view.findViewById(R.id.list); streamInfoListLayoutManager = new LinearLayoutManager(context); recyclerView.setLayoutManager(streamInfoListLayoutManager); @@ -219,19 +156,16 @@ public class SearchInfoItemFragment extends Fragment { getActivity().findViewById(android.R.id.content)); infoListAdapter.setFooter(inflater.inflate(R.layout.pignate_footer, recyclerView, false)); infoListAdapter.showFooter(false); - infoListAdapter.setOnStreamInfoItemSelectedListener( - new InfoItemBuilder.OnInfoItemSelectedListener() { + infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override - public void selected(String url, int serviceId) { - NavStack.getInstance() - .openDetailActivity(getContext(), url, serviceId); + public void selected(int serviceId, String url, String title) { + NavigationHelper.openVideoDetail(onItemSelectedListener, serviceId, url, title); } }); infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override - public void selected(String url, int serviceId) { - NavStack.getInstance() - .openChannelActivity(getContext(), url, serviceId); + public void selected(int serviceId, String url, String title) { + NavigationHelper.openChannel(onItemSelectedListener, serviceId, url, title); } }); recyclerView.setAdapter(infoListAdapter); @@ -249,6 +183,13 @@ public class SearchInfoItemFragment extends Fragment { if ((visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading) { pageNumber++; + recyclerView.post(new Runnable() { + @Override + public void run() { + infoListAdapter.showFooter(true); + + } + }); search(searchQuery, pageNumber); } } @@ -259,13 +200,35 @@ public class SearchInfoItemFragment extends Fragment { } @Override - public void onStart() { - super.onStart(); - if(!searchQuery.isEmpty()) { + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (!searchQuery.isEmpty()) { search(searchQuery); } } + @Override + public void onDestroyView() { + super.onDestroyView(); + recyclerView.removeAllViews(); + infoListAdapter.clearSteamItemList(); + recyclerView = null; + } + + @Override + public void onResume() { + super.onResume(); + if (isLoading && !searchQuery.isEmpty()) { + search(searchQuery); + } + } + + @Override + public void onStop() { + super.onStop(); + SearchWorker.getInstance().terminate(); + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -273,19 +236,45 @@ public class SearchInfoItemFragment extends Fragment { outState.putInt(STREAMING_SERVICE, streamingServiceId); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case RECAPTCHA_REQUEST: + if (resultCode == RESULT_OK && searchQuery.length() != 0) { + search(searchQuery); + } else Log.e(TAG, "ReCaptcha failed"); + break; + + default: + Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); + break; + } + } + + /*////////////////////////////////////////////////////////////////////////// + // Menu + //////////////////////////////////////////////////////////////////////////*/ + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); + ActionBar supportActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayHomeAsUpEnabled(false); + supportActionBar.setDisplayShowTitleEnabled(false); + //noinspection deprecation + supportActionBar.setNavigationMode(0); + } inflater.inflate(R.menu.search_menu, menu); MenuItem searchItem = menu.findItem(R.id.action_search); - SearchView searchView = (SearchView) searchItem.getActionView(); + searchView = (SearchView) searchItem.getActionView(); setupSearchView(searchView); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { + switch (item.getItemId()) { case R.id.menu_filter_all: changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL)); return true; @@ -300,11 +289,15 @@ public class SearchInfoItemFragment extends Fragment { } } + /*////////////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////////////*/ + private void changeFilter(MenuItem item, EnumSet filter) { this.filter = filter; item.setChecked(true); - if(searchQuery != null && !searchQuery.isEmpty()) { - Log.d(TAG, "Fuck+ " + searchQuery); + if (searchQuery != null && !searchQuery.isEmpty()) { + Log.e(TAG, "Fuck+ " + searchQuery); search(searchQuery); } } @@ -313,7 +306,7 @@ public class SearchInfoItemFragment extends Fragment { suggestionListAdapter = new SuggestionListAdapter(getActivity()); searchView.setSuggestionsAdapter(suggestionListAdapter); searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView, suggestionListAdapter)); - searchView.setOnQueryTextListener(new SearchQueryListener()); + searchView.setOnQueryTextListener(this); if (searchQuery != null && !searchQuery.isEmpty()) { searchView.setQuery(searchQuery, false); searchView.setIconifiedByDefault(false); @@ -341,9 +334,9 @@ public class SearchInfoItemFragment extends Fragment { } private void setDoneLoading() { - this.isLoading = false; + isLoading = false; loadingIndicator.setVisibility(View.GONE); - infoListAdapter.showFooter(true); + infoListAdapter.showFooter(false); } /** @@ -351,7 +344,7 @@ public class SearchInfoItemFragment extends Fragment { */ private void hideBackground() { View view = getView(); - if(view == null) return; + if (view == null) return; view.findViewById(R.id.mainBG).setVisibility(View.GONE); } @@ -362,22 +355,86 @@ public class SearchInfoItemFragment extends Fragment { suggestionThread.start(); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case RECAPTCHA_REQUEST: - if (resultCode == RESULT_OK) { - if (searchQuery.length() != 0) { - search(searchQuery); - } - } else { - Log.d(TAG, "ReCaptcha failed"); - } - break; - - default: - Log.e(TAG, "Request code from activity not supported [" + requestCode + "]"); - break; - } + public boolean isMainBgVisible() { + return getActivity().findViewById(R.id.mainBG).getVisibility() == View.VISIBLE; } + + /*////////////////////////////////////////////////////////////////////////// + // OnQueryTextListener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public boolean onQueryTextSubmit(String query) { + Activity a = getActivity(); + try { + 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, + NewPipe.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(); + } + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (!newText.isEmpty()) { + searchSuggestions(newText); + } + return true; + } + + /*////////////////////////////////////////////////////////////////////////// + // SearchWorkerResultListener + //////////////////////////////////////////////////////////////////////////*/ + + @Override + public void onResult(SearchResult result) { + infoListAdapter.addInfoItemList(result.resultList); + setDoneLoading(); + } + + @Override + public void onNothingFound(int stringResource) { + //setListShown(true); + Toast.makeText(getActivity(), getString(stringResource), Toast.LENGTH_SHORT).show(); + setDoneLoading(); + } + + @Override + public void onError(String message) { + //setListShown(true); + Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show(); + setDoneLoading(); + } + + @Override + public void onReCaptchaChallenge() { + Toast.makeText(getActivity(), "ReCaptcha Challenge requested", + Toast.LENGTH_LONG).show(); + + // Starting ReCaptcha Challenge Activity + startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), RECAPTCHA_REQUEST); + } + } diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchSuggestionListener.java similarity index 97% rename from app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java rename to app/src/main/java/org/schabi/newpipe/fragments/search/SearchSuggestionListener.java index a3d3c0e9b..9e2a74dad 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchSuggestionListener.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchSuggestionListener.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.search_fragment; +package org.schabi.newpipe.fragments.search; import android.support.v7.widget.SearchView; diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchWorker.java similarity index 99% rename from app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java rename to app/src/main/java/org/schabi/newpipe/fragments/search/SearchWorker.java index 6a18f42dd..582df9950 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SearchWorker.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchWorker.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.search_fragment; +package org.schabi.newpipe.fragments.search; import android.app.Activity; import android.content.SharedPreferences; @@ -7,13 +7,13 @@ import android.preference.PreferenceManager; import android.util.Log; import android.view.View; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.search.SearchEngine; import org.schabi.newpipe.extractor.search.SearchResult; import org.schabi.newpipe.report.ErrorActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.NewPipe; import java.io.IOException; import java.util.EnumSet; @@ -209,6 +209,7 @@ public class SearchWorker { } public void terminate() { + if (runnable == null) return; requestId++; runnable.terminate(); } diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionListAdapter.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java rename to app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionListAdapter.java index c37a71895..b8ecaad1d 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionListAdapter.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.search_fragment; +package org.schabi.newpipe.fragments.search; import android.content.Context; import android.database.Cursor; diff --git a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionSearchRunnable.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java rename to app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionSearchRunnable.java index 8b8f75e7e..10800c7e3 100644 --- a/app/src/main/java/org/schabi/newpipe/search_fragment/SuggestionSearchRunnable.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/SuggestionSearchRunnable.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe.search_fragment; +package org.schabi.newpipe.fragments.search; import android.app.Activity; import android.content.SharedPreferences; @@ -6,11 +6,11 @@ import android.os.Handler; import android.preference.PreferenceManager; import android.widget.Toast; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.SuggestionExtractor; -import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.SuggestionExtractor; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.report.ErrorActivity; import java.io.IOException; import java.util.List; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index 4ec6123ad..20dda18a2 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -1,11 +1,10 @@ package org.schabi.newpipe.info_list; -import android.app.Activity; +import android.content.Context; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; @@ -39,20 +38,21 @@ import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; public class InfoItemBuilder { - final String viewsS; - final String videosS; - final String subsS; + private final String viewsS; + private final String videosS; + private final String subsS; + private final String subsPluralS; - final String thousand; - final String million; - final String billion; + private final String thousand; + private final String million; + private final String billion; private static final String TAG = InfoItemBuilder.class.toString(); public interface OnInfoItemSelectedListener { - void selected(String url, int serviceId); + void selected(int serviceId, String url, String title); } - private Activity activity = null; + private Context mContext = null; private View rootView = null; private ImageLoader imageLoader = ImageLoader.getInstance(); private DisplayImageOptions displayImageOptions = @@ -60,15 +60,16 @@ public class InfoItemBuilder { private OnInfoItemSelectedListener onStreamInfoItemSelectedListener; private OnInfoItemSelectedListener onChannelInfoItemSelectedListener; - public InfoItemBuilder(Activity a, View rootView) { - activity = a; + public InfoItemBuilder(Context context, View rootView) { + mContext = context; this.rootView = rootView; - viewsS = a.getString(R.string.views); - videosS = a.getString(R.string.videos); - subsS = a.getString(R.string.subscriber); - thousand = a.getString(R.string.short_thousand); - million = a.getString(R.string.short_million); - billion = a.getString(R.string.short_billion); + viewsS = context.getString(R.string.views); + videosS = context.getString(R.string.videos); + subsS = context.getString(R.string.subscriber); + subsPluralS = context.getString(R.string.subscriber_plural); + thousand = context.getString(R.string.short_thousand); + million = context.getString(R.string.short_million); + billion = context.getString(R.string.short_billion); } public void setOnStreamInfoItemSelectedListener( @@ -156,13 +157,13 @@ public class InfoItemBuilder { imageLoader.displayImage(info.thumbnail_url, holder.itemThumbnailView, displayImageOptions, - new ImageErrorLoadingListener(activity, rootView, info.service_id)); + new ImageErrorLoadingListener(mContext, rootView, info.service_id)); } holder.itemButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onStreamInfoItemSelectedListener.selected(info.webpage_url, info.service_id); + onStreamInfoItemSelectedListener.selected(info.service_id, info.webpage_url, info.getTitle()); } }); } @@ -178,13 +179,13 @@ public class InfoItemBuilder { imageLoader.displayImage(info.thumbnailUrl, holder.itemThumbnailView, displayImageOptions, - new ImageErrorLoadingListener(activity, rootView, info.serviceId)); + new ImageErrorLoadingListener(mContext, rootView, info.serviceId)); } holder.itemButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onChannelInfoItemSelectedListener.selected(info.getLink(), info.serviceId); + onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName); } }); } @@ -202,15 +203,17 @@ public class InfoItemBuilder { } } - public String shortSubscriber(Long count){ - if(count >= 1000000000){ - return Long.toString(count/1000000000)+ billion + " " + subsS; - }else if(count>=1000000){ - return Long.toString(count/1000000)+ million + " " + subsS; - }else if(count>=1000){ - return Long.toString(count/1000)+ thousand + " " + subsS; - }else { - return Long.toString(count)+ " " + subsS; + public String shortSubscriber(Long count) { + String curSubString = count > 1 ? subsPluralS : subsS; + + if (count >= 1000000000) { + return Long.toString(count / 1000000000) + billion + " " + curSubString; + } else if (count >= 1000000) { + return Long.toString(count / 1000000) + million + " " + curSubString; + } else if (count >= 1000) { + return Long.toString(count / 1000) + thousand + " " + curSubString; + } else { + return Long.toString(count) + " " + curSubString; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java index 1c6d4822b..e05477b98 100644 --- a/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/AbstractPlayer.java @@ -299,7 +299,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa return; } - changeState(STATE_LOADING); isPrepared = false; qualityChanged = false; @@ -312,6 +311,7 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos); simpleExoPlayer.prepare(videoSource); simpleExoPlayer.setPlayWhenReady(autoPlay); + changeState(STATE_LOADING); } public void destroy() { @@ -396,7 +396,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN); animateView(endScreen, false, 0, 0); - animateView(controlsRoot, false, 0, 0); loadingPanel.setBackgroundColor(Color.BLACK); animateView(loadingPanel, true, 0, 0); animateView(surfaceForeground, true, 100, 0); @@ -408,7 +407,12 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (!isProgressLoopRunning.get()) startProgressLoop(); showAndAnimateControl(-1, true); loadingPanel.setVisibility(View.GONE); - animateView(controlsRoot, false, 500, DEFAULT_CONTROLS_HIDE_TIME, true); + animateView(controlsRoot, true, 500, 0, new Runnable() { + @Override + public void run() { + animateView(controlsRoot, false, 500, DEFAULT_CONTROLS_HIDE_TIME, true); + } + }); animateView(currentDisplaySeek, false, 200, 0); } @@ -417,7 +421,6 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (DEBUG) Log.d(TAG, "onBuffering() called"); loadingPanel.setBackgroundColor(Color.TRANSPARENT); animateView(loadingPanel, true, 500, 0); - animateView(controlsRoot, false, 0, 0, true); } @Override @@ -598,14 +601,12 @@ public abstract class AbstractPlayer implements StateInterface, SeekBar.OnSeekBa if (DEBUG) Log.d(TAG, "onFastRewind() called"); seekBy(-FAST_FORWARD_REWIND_AMOUNT); showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true); - animateView(controlsRoot, false, 100, 0); } public void onFastForward() { if (DEBUG) Log.d(TAG, "onFastForward() called"); seekBy(FAST_FORWARD_REWIND_AMOUNT); showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true); - animateView(controlsRoot, false, 100, 0); } /*////////////////////////////////////////////////////////////////////////// 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 88a5a914d..3c6bbad81 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -24,9 +24,10 @@ import android.widget.Toast; import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.util.Constants; import java.io.IOException; import java.util.Arrays; @@ -353,10 +354,11 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare new Intent(ACTION_REWIND), PendingIntent.FLAG_UPDATE_CURRENT); //build intent to return to video, on tapping notification - Intent openDetailViewIntent = new Intent(getApplicationContext(), - VideoItemDetailActivity.class); - openDetailViewIntent.putExtra(NavStack.SERVICE_ID, serviceId); - openDetailViewIntent.putExtra(NavStack.URL, webUrl); + Intent openDetailViewIntent = new Intent(getApplicationContext(), MainActivity.class); + openDetailViewIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); + openDetailViewIntent.putExtra(Constants.KEY_URL, webUrl); + openDetailViewIntent.putExtra(Constants.KEY_TITLE, title); + openDetailViewIntent.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID, openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT); diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java index 3a7c5e085..435fd189b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -24,7 +24,6 @@ import android.widget.TextView; import android.widget.Toast; import org.schabi.newpipe.R; -import org.schabi.newpipe.util.NavStack; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -70,6 +69,7 @@ public class ExoPlayerActivity extends Activity { return; } + showSystemUi(); setContentView(R.layout.activity_exo_player); playerImpl = new AbstractPlayerImpl(); playerImpl.setup(findViewById(android.R.id.content)); @@ -88,7 +88,6 @@ public class ExoPlayerActivity extends Activity { public void onBackPressed() { if (DEBUG) Log.d(TAG, "onBackPressed() called"); super.onBackPressed(); - if (playerImpl.isStartedFromNewPipe()) NavStack.getInstance().openDetailActivity(this, playerImpl.getVideoUrl(), 0); if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false); } @@ -340,8 +339,7 @@ public class ExoPlayerActivity extends Activity { public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); if (playerImpl.wasPlaying()) { - hideSystemUi(); - playerImpl.getControlsRoot().setVisibility(View.GONE); + animateView(playerImpl.getControlsRoot(), false, 100, 0); } } @@ -365,6 +363,13 @@ public class ExoPlayerActivity extends Activity { public void onLoading() { super.onLoading(); playPauseButton.setImageResource(R.drawable.ic_pause_white); + animateView(playPauseButton, false, 100, 0); + } + + @Override + public void onBuffering() { + super.onBuffering(); + animateView(playPauseButton, false, 100, 0); } @Override @@ -384,6 +389,7 @@ public class ExoPlayerActivity extends Activity { public void onPlaying() { super.onPlaying(); animateView(playPauseButton, true, 500, 0); + showSystemUi(); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 090179a7a..6e4fa9776 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -34,16 +34,17 @@ import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListene import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; -import org.schabi.newpipe.detail.VideoItemDetailActivity; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream_info.StreamExtractor; import org.schabi.newpipe.extractor.stream_info.StreamInfo; import org.schabi.newpipe.extractor.stream_info.VideoStream; -import org.schabi.newpipe.util.NavStack; +import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.util.Utils; import java.io.IOException; import java.util.ArrayList; @@ -107,7 +108,7 @@ public class PopupVideoPlayer extends Service { if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true); if (imageLoader != null) imageLoader.clearMemoryCache(); - if (intent.getStringExtra(NavStack.URL) != null) { + if (intent.getStringExtra(Constants.KEY_URL) != null) { playerImpl.setStartedFromNewPipe(false); Thread fetcher = new Thread(new FetcherRunnable(intent)); fetcher.start(); @@ -158,7 +159,7 @@ public class PopupVideoPlayer extends Service { playerImpl.onVideoPlayPause(); break; case ACTION_OPEN_DETAIL: - onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl()); + onOpenDetail(PopupVideoPlayer.this, playerImpl.getVideoUrl(), playerImpl.getVideoTitle()); break; case ACTION_REPEAT: playerImpl.onRepeatClicked(); @@ -266,12 +267,14 @@ public class PopupVideoPlayer extends Service { stopSelf(); } - public void onOpenDetail(Context context, String videoUrl) { + public void onOpenDetail(Context context, String videoUrl, String videoTitle) { if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]"); - Intent i = new Intent(context, VideoItemDetailActivity.class); - i.putExtra(NavStack.SERVICE_ID, 0) - .putExtra(NavStack.URL, videoUrl) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent i = new Intent(context, MainActivity.class); + i.putExtra(Constants.KEY_SERVICE_ID, 0); + i.putExtra(Constants.KEY_URL, videoUrl); + i.putExtra(Constants.KEY_TITLE, videoTitle); + i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(i); context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); } @@ -510,8 +513,6 @@ public class PopupVideoPlayer extends Service { private class FetcherRunnable implements Runnable { private final Intent intent; private final Handler mainHandler; - private final boolean printStreams = true; - FetcherRunnable(Intent intent) { this.intent = intent; @@ -524,48 +525,22 @@ public class PopupVideoPlayer extends Service { try { StreamingService service = NewPipe.getService(0); if (service == null) return; - streamExtractor = service.getExtractorInstance(intent.getStringExtra(NavStack.URL)); + streamExtractor = service.getExtractorInstance(intent.getStringExtra(Constants.KEY_URL)); StreamInfo info = StreamInfo.getVideoInfo(streamExtractor); - String defaultResolution = playerImpl.getSharedPreferences().getString( - getResources().getString(R.string.default_resolution_key), - getResources().getString(R.string.default_resolution_value)); - - VideoStream chosen = null, secondary = null, fallback = null; playerImpl.setVideoStreamsList(info.video_streams instanceof ArrayList ? (ArrayList) info.video_streams : new ArrayList<>(info.video_streams)); - for (VideoStream item : info.video_streams) { - if (DEBUG && printStreams) { - Log.d(TAG, "FetcherRunnable.StreamExtractor: current Item" - + ", item.resolution = " + item.resolution - + ", item.format = " + item.format - + ", item.url = " + item.url); - } - if (defaultResolution.equals(item.resolution)) { - if (item.format == MediaFormat.MPEG_4.id) { - chosen = item; - if (DEBUG) Log.d(TAG, "FetcherRunnable.StreamExtractor: CHOSEN item, item.resolution = " + item.resolution + ", item.format = " + item.format + ", item.url = " + item.url); - } else if (item.format == 2) secondary = item; - else fallback = item; - } + int defaultResolution = Utils.getPreferredResolution(PopupVideoPlayer.this, info.video_streams); + playerImpl.setSelectedIndexStream(defaultResolution); + + if (DEBUG) { + Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + + MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " " + + info.video_streams.get(defaultResolution).resolution + " > " + + info.video_streams.get(defaultResolution).url); } - int selectedIndexStream; - - if (chosen != null) selectedIndexStream = info.video_streams.indexOf(chosen); - else if (secondary != null) selectedIndexStream = info.video_streams.indexOf(secondary); - else if (fallback != null) selectedIndexStream = info.video_streams.indexOf(fallback); - else selectedIndexStream = 0; - - playerImpl.setSelectedIndexStream(selectedIndexStream); - - if (DEBUG && printStreams) Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = " + chosen - + "\n, secondary = " + secondary - + "\n, fallback = " + fallback - + "\n, info.video_streams.get(0).url = " + info.video_streams.get(0).url); - - playerImpl.setVideoUrl(info.webpage_url); playerImpl.setVideoTitle(info.title); playerImpl.setChannelName(info.uploader); @@ -578,6 +553,8 @@ public class PopupVideoPlayer extends Service { playerImpl.playVideo(playerImpl.getSelectedStreamUri(), true); } }); + + imageLoader.resume(); imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() { @Override public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) { diff --git a/app/src/main/java/org/schabi/newpipe/util/Constants.java b/app/src/main/java/org/schabi/newpipe/util/Constants.java new file mode 100644 index 000000000..9f94729ae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/Constants.java @@ -0,0 +1,8 @@ +package org.schabi.newpipe.util; + +public class Constants { + public static final String KEY_SERVICE_ID = "key_service_id"; + public static final String KEY_URL = "key_url"; + public static final String KEY_TITLE = "key_title"; + public static final String KEY_LINK_TYPE = "key_link_type"; +} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavStack.java b/app/src/main/java/org/schabi/newpipe/util/NavStack.java deleted file mode 100644 index d06aafe7e..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/NavStack.java +++ /dev/null @@ -1,148 +0,0 @@ -package org.schabi.newpipe.util; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.NavUtils; - -import org.schabi.newpipe.ChannelActivity; -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.detail.VideoItemDetailActivity; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; - -import java.util.ArrayList; -import java.util.Stack; - -/** - * Created by Christian Schabesberger on 16.02.17. - * - * Copyright (C) Christian Schabesberger 2016 - * NavStack.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 helps to navigate within the app - * IMPORTAND: the top of the stack is the current activity !!! - */ -public class NavStack { - private static final String TAG = NavStack.class.toString(); - public static final String SERVICE_ID = "service_id"; - public static final String URL = "url"; - - private static final String NAV_STACK="nav_stack"; - - private enum ActivityId { - CHANNEL, - DETAIL - } - - private class NavEntry { - public NavEntry(String url, int serviceId) { - this.url = url; - this.serviceId = serviceId; - } - public String url; - public int serviceId; - } - - private static NavStack instance = new NavStack(); - private Stack stack = new Stack(); - - private NavStack() { - } - - public static NavStack getInstance() { - return instance; - } - - public void navBack(Activity activity) throws Exception { - if(stack.size() == 0) { // if stack is already empty here, activity was probably called - // from another app - activity.finish(); - return; - } - stack.pop(); // remove curent activty, since we dont want to return to itself - if (stack.size() == 0) { - openMainActivity(activity); // if no more page is on the stack this means we are home - return; - } - NavEntry entry = stack.pop(); // this element will reapear, since by calling the old page - // this element will be pushed on top again - try { - StreamingService service = NewPipe.getService(entry.serviceId); - switch (service.getLinkTypeByUrl(entry.url)) { - case STREAM: - openDetailActivity(activity, entry.url, entry.serviceId); - break; - case CHANNEL: - openChannelActivity(activity, entry.url, entry.serviceId); - break; - case NONE: - throw new Exception("Url not known to service. service=" - + Integer.toString(entry.serviceId) + " url=" + entry.url); - default: - openMainActivity(activity); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - - public void openChannelActivity(Context context, String url, int serviceId) { - openActivity(context, url, serviceId, ChannelActivity.class); - } - - public void openDetailActivity(Context context, String url, int serviceId) { - openActivity(context, url, serviceId, VideoItemDetailActivity.class); - } - - private void openActivity(Context context, String url, int serviceId, Class acitivtyClass) { - //if last element has the same url do not push to stack again - if(stack.isEmpty() || !stack.peek().url.equals(url)) { - stack.push(new NavEntry(url, serviceId)); - } - Intent i = new Intent(context, acitivtyClass); - i.putExtra(SERVICE_ID, serviceId); - i.putExtra(URL, url); - context.startActivity(i); - } - - public void openMainActivity(Activity a) { - stack.clear(); - Intent i = new Intent(a, MainActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - NavUtils.navigateUpTo(a, i); - } - - public void onSaveInstanceState(Bundle state) { - ArrayList sa = new ArrayList<>(); - for(NavEntry entry : stack) { - sa.add(entry.url); - } - state.putStringArrayList(NAV_STACK, sa); - } - - public void restoreSavedInstanceState(Bundle state) { - ArrayList sa = state.getStringArrayList(NAV_STACK); - stack.clear(); - for(String url : sa) { - stack.push(new NavEntry(url, NewPipe.getServiceByUrl(url).getServiceId())); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java new file mode 100644 index 000000000..17708dbef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -0,0 +1,96 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.content.Intent; +import android.preference.PreferenceManager; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.fragments.OnItemSelectedListener; +import org.schabi.newpipe.fragments.detail.VideoDetailFragment; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class NavigationHelper { + + /*////////////////////////////////////////////////////////////////////////// + // Through Interface (faster) + //////////////////////////////////////////////////////////////////////////*/ + + public static void openChannel(OnItemSelectedListener listener, int serviceId, String url) { + openChannel(listener, serviceId, url, null); + } + + public static void openChannel(OnItemSelectedListener listener, int serviceId, String url, String name) { + listener.onItemSelected(StreamingService.LinkType.CHANNEL, serviceId, url, name); + } + + public static void openVideoDetail(OnItemSelectedListener listener, int serviceId, String url) { + openVideoDetail(listener, serviceId, url, null); + } + + public static void openVideoDetail(OnItemSelectedListener listener, int serviceId, String url, String title) { + listener.onItemSelected(StreamingService.LinkType.STREAM, serviceId, url, title); + } + + /*////////////////////////////////////////////////////////////////////////// + // Through Intents + //////////////////////////////////////////////////////////////////////////*/ + + public static void openByLink(Context context, String url) throws Exception { + context.startActivity(getIntentByLink(context, url)); + } + + public static void openChannel(Context context, int serviceId, String url) { + openChannel(context, serviceId, url, null); + } + + public static void openChannel(Context context, int serviceId, String url, String name) { + Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL); + if (name != null && !name.isEmpty()) openIntent.putExtra(Constants.KEY_TITLE, name); + context.startActivity(openIntent); + } + + public static void openVideoDetail(Context context, int serviceId, String url) { + openVideoDetail(context, serviceId, url, null); + } + + public static void openVideoDetail(Context context, int serviceId, String url, String title) { + Intent openIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM); + if (title != null && !title.isEmpty()) openIntent.putExtra(Constants.KEY_TITLE, title); + context.startActivity(openIntent); + } + + public static void openMainActivity(Context context) { + Intent mIntent = new Intent(context, MainActivity.class); + context.startActivity(mIntent); + } + + private static Intent getOpenIntent(Context context, String url, int serviceId, StreamingService.LinkType type) { + Intent mIntent = new Intent(context, MainActivity.class); + mIntent.putExtra(Constants.KEY_SERVICE_ID, serviceId); + mIntent.putExtra(Constants.KEY_URL, url); + mIntent.putExtra(Constants.KEY_LINK_TYPE, type); + return mIntent; + } + + private static Intent getIntentByLink(Context context, String url) throws Exception { + StreamingService service = NewPipe.getServiceByUrl(url); + if (service == null) throw new Exception("NewPipe.getServiceByUrl returned null for url > \"" + url + "\""); + int serviceId = service.getServiceId(); + switch (service.getLinkTypeByUrl(url)) { + case STREAM: + Intent sIntent = getOpenIntent(context, url, serviceId, StreamingService.LinkType.STREAM); + sIntent.putExtra(VideoDetailFragment.AUTO_PLAY, PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.autoplay_through_intent_key), false)); + return sIntent; + case CHANNEL: + return getOpenIntent(context, url, serviceId, StreamingService.LinkType.CHANNEL); + case NONE: + throw new Exception("Url not known to service. service=" + + Integer.toString(serviceId) + " url=" + url); + } + return null; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/Utils.java b/app/src/main/java/org/schabi/newpipe/util/Utils.java new file mode 100644 index 000000000..98d8bf25b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/Utils.java @@ -0,0 +1,91 @@ +package org.schabi.newpipe.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream_info.AudioStream; +import org.schabi.newpipe.extractor.stream_info.VideoStream; + +import java.util.List; + +public class Utils { + + /** + * Return the index of the default stream in the list, based on the + * preferred resolution and format chosen in the settings + * + * @param videoStreams the list that will be extracted the index + * @return index of the preferred resolution&format + */ + public static int getPreferredResolution(Context context, List videoStreams) { + SharedPreferences defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (defaultPreferences == null) return 0; + + String defaultResolution = defaultPreferences + .getString(context.getString(R.string.default_resolution_key), + context.getString(R.string.default_resolution_value)); + + String preferredFormat = defaultPreferences + .getString(context.getString(R.string.preferred_video_format_key), + context.getString(R.string.preferred_video_format_default)); + + // first try to find the one with the right resolution + int selectedFormat = 0; + for (int i = 0; i < videoStreams.size(); i++) { + VideoStream item = videoStreams.get(i); + if (defaultResolution.equals(item.resolution)) { + selectedFormat = i; + } + } + + // than try to find the one with the right resolution and format + for (int i = 0; i < videoStreams.size(); i++) { + VideoStream item = videoStreams.get(i); + if (defaultResolution.equals(item.resolution) + && preferredFormat.equals(MediaFormat.getNameById(item.format))) { + selectedFormat = i; + } + } + + // this is actually an error, + // but maybe there is really no stream fitting to the default value. + return selectedFormat; + } + + /** + * Return the index of the default stream in the list, based on the + * preferred audio format chosen in the settings + * + * @param audioStreams the list that will be extracted the index + * @return index of the preferred format + */ + public static int getPreferredAudioFormat(Context context, List audioStreams) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (sharedPreferences == null) return 0; + + String preferredFormatString = sharedPreferences.getString(context.getString(R.string.default_audio_format_key), "webm"); + + int preferredFormat = MediaFormat.WEBMA.id; + switch (preferredFormatString) { + case "webm": + preferredFormat = MediaFormat.WEBMA.id; + break; + case "m4a": + preferredFormat = MediaFormat.M4A.id; + break; + default: + break; + } + + for (int i = 0; i < audioStreams.size(); i++) { + if (audioStreams.get(i).format == preferredFormat) { + return i; + } + } + + return 0; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java new file mode 100644 index 000000000..6b815b493 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/workers/ChannelExtractorWorker.java @@ -0,0 +1,102 @@ +package org.schabi.newpipe.workers; + +import android.content.Context; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.channel.ChannelExtractor; +import org.schabi.newpipe.extractor.channel.ChannelInfo; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.report.ErrorActivity; + +import java.io.IOException; + +/** + * Extract {@link ChannelInfo} with {@link ChannelExtractor} from the given url of the given service + * + * @author mauriciocolli + */ +@SuppressWarnings("WeakerAccess") +public class ChannelExtractorWorker extends ExtractorWorker { + //private static final String TAG = "ChannelExtractorWorker"; + + private int pageNumber; + private boolean onlyVideos; + + private ChannelInfo channelInfo = null; + private OnChannelInfoReceive callback; + + /** + * Interface which will be called for result and errors + */ + public interface OnChannelInfoReceive { + void onReceive(ChannelInfo info); + void onError(int messageId); + } + + /** + * @param context context for error reporting purposes + * @param serviceId id of the request service + * @param channelUrl channelUrl of the service (e.g. https://www.youtube.com/channel/UC_aEa8K-EOJ3D6gOs7HcyNg) + * @param callback listener that will be called-back when events occur (check {@link ChannelExtractorWorker.OnChannelInfoReceive}) + */ + public ChannelExtractorWorker(Context context, int serviceId, String channelUrl, int pageNumber, OnChannelInfoReceive callback) { + super(context, channelUrl, serviceId); + this.pageNumber = pageNumber; + this.callback = callback; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + this.callback = null; + this.channelInfo = null; + } + + @Override + protected void doWork(int serviceId, String url) throws Exception { + ChannelExtractor extractor = getService().getChannelExtractorInstance(url, pageNumber); + channelInfo = ChannelInfo.getInfo(extractor); + + if (!channelInfo.errors.isEmpty()) handleErrorsDuringExtraction(channelInfo.errors, ErrorActivity.REQUESTED_CHANNEL); + + if (callback != null && channelInfo != null && !isInterrupted()) getHandler().post(new Runnable() { + @Override + public void run() { + if (isInterrupted() || callback == null) return; + + callback.onReceive(channelInfo); + onDestroy(); + } + }); + } + + + @Override + protected void handleException(Exception exception, int serviceId, String url) { + if (exception instanceof IOException) { + if (callback != null) getHandler().post(new Runnable() { + @Override + public void run() { + callback.onError(R.string.network_error); + } + }); + } else if (exception instanceof ParsingException || exception instanceof ExtractionException) { + ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, getServiceName(), url, R.string.parsing_error)); + finishIfActivity(); + } else { + ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_CHANNEL, getServiceName(), url, R.string.general_error)); + finishIfActivity(); + } + } + + public boolean isOnlyVideos() { + return onlyVideos; + } + + public void setOnlyVideos(boolean onlyVideos) { + this.onlyVideos = onlyVideos; + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java new file mode 100644 index 000000000..b26d62ae8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/workers/ExtractorWorker.java @@ -0,0 +1,168 @@ +package org.schabi.newpipe.workers; + +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.util.Log; +import android.view.View; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.report.ErrorActivity; + +import java.io.InterruptedIOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Common properties of ExtractorWorkers + * + * @author mauriciocolli + */ +@SuppressWarnings("WeakerAccess") +public abstract class ExtractorWorker extends Thread { + + private final AtomicBoolean isRunning = new AtomicBoolean(false); + + private final String url; + private final int serviceId; + private Context context; + private Handler handler; + private StreamingService service; + + public ExtractorWorker(Context context, String url, int serviceId) { + this.context = context; + this.url = url; + this.serviceId = serviceId; + this.handler = new Handler(context.getMainLooper()); + if (url.length() >= 40) setName("Thread-" + url.substring(url.length() - 11, url.length())); + } + + @Override + public void run() { + try { + isRunning.set(true); + service = NewPipe.getService(serviceId); + doWork(serviceId, url); + } catch (Exception e) { + // Handle the exception only if thread is not interrupted + if (!isInterrupted() && !(e instanceof InterruptedIOException) && !(e.getCause() instanceof InterruptedIOException)) { + handleException(e, serviceId, url); + } + } finally { + isRunning.set(false); + } + } + + /** + * Here is the place that the heavy work is realized + * + * @param serviceId serviceId that was passed when created this object + * @param url url that was passed when created this object + * + * @throws Exception these exceptions are handled by the {@link #handleException(Exception, int, String)} + */ + protected abstract void doWork(int serviceId, String url) throws Exception; + + + /** + * Method that handle the exception thrown by the {@link #doWork(int, String)}. + * + * @param exception {@link Exception} that was thrown by {@link #doWork(int, String)} + */ + protected abstract void handleException(Exception exception, int serviceId, String url); + + /** + * Handle the errors during extraction and shows a Report button to the user.
+ * Subclasses maybe call this method. + * + * @param errorsList list of exceptions that happened during extraction + * @param errorUserAction what action was the user performing during the error. + * (One of the {@link ErrorActivity}.REQUEST_* error (message) ids) + */ + protected void handleErrorsDuringExtraction(List errorsList, int errorUserAction){ + String errorString = ""; + switch (errorUserAction) { + case ErrorActivity.REQUESTED_STREAM: + errorString= ErrorActivity.REQUESTED_STREAM_STRING; + break; + case ErrorActivity.REQUESTED_CHANNEL: + errorString= ErrorActivity.REQUESTED_CHANNEL_STRING; + break; + } + + Log.e(errorString, "OCCURRED ERRORS DURING EXTRACTION:"); + for (Throwable e : errorsList) { + e.printStackTrace(); + Log.e(errorString, "------"); + } + + if (getContext() instanceof Activity) { + View rootView = getContext() != null ? ((Activity) getContext()).findViewById(android.R.id.content) : null; + ErrorActivity.reportError(getHandler(), getContext(), errorsList, null, rootView, ErrorActivity.ErrorInfo.make(errorUserAction, getServiceName(), url, 0 /* no message for the user */)); + } + } + + /** + * Return true if the extraction is not completed yet + * + * @return the value of the AtomicBoolean {@link #isRunning} + */ + public boolean isRunning() { + return isRunning.get(); + } + + /** + * Cancel this ExtractorWorker, calling {@link #onDestroy()} and interrupting this thread. + *

+ * Note: Any I/O that is active in the moment that this method is called will be canceled and a Exception will be thrown, because of the {@link #interrupt()}.
+ * This is useful when you don't want the resulting {@link StreamInfo} anymore, but don't want to waste bandwidth, otherwise it'd run till it receives the StreamInfo. + */ + public void cancel() { + onDestroy(); + this.interrupt(); + } + + /** + * Method that discards everything that doesn't need anymore.
+ * Subclasses can override this method to destroy their garbage. + */ + protected void onDestroy() { + this.isRunning.set(false); + this.context = null; + this.handler = null; + this.service = null; + } + + /** + * If the context passed in the constructor is an {@link Activity}, finish it. + */ + protected void finishIfActivity() { + if (getContext() instanceof Activity) ((Activity) getContext()).finish(); + } + + public Handler getHandler() { + return handler; + } + + public String getUrl() { + return url; + } + + public StreamingService getService() { + return service; + } + + public int getServiceId() { + return serviceId; + } + + public String getServiceName() { + return service == null ? "none" : service.getServiceInfo().name; + } + + public Context getContext() { + return context; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java b/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java new file mode 100644 index 000000000..bc65f8dc8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/workers/StreamExtractorWorker.java @@ -0,0 +1,136 @@ +package org.schabi.newpipe.workers; + +import android.content.Context; + +import org.schabi.newpipe.MainActivity; +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.stream_info.StreamExtractor; +import org.schabi.newpipe.extractor.stream_info.StreamInfo; +import org.schabi.newpipe.report.ErrorActivity; + +import java.io.IOException; + +/** + * Extract {@link StreamInfo} with {@link StreamExtractor} from the given url of the given service + * + * @author mauriciocolli + */ +@SuppressWarnings("WeakerAccess") +public class StreamExtractorWorker extends ExtractorWorker { + //private static final String TAG = "StreamExtractorWorker"; + + private StreamInfo streamInfo = null; + private OnStreamInfoReceivedListener callback; + + /** + * Interface which will be called for result and errors + */ + public interface OnStreamInfoReceivedListener { + void onReceive(StreamInfo info); + void onError(int messageId); + void onReCaptchaException(); + void onBlockedByGemaError(); + void onContentErrorWithMessage(int messageId); + void onContentError(); + } + + /** + * @param context context for error reporting purposes + * @param serviceId id of the request service + * @param videoUrl videoUrl of the service (e.g. https://www.youtube.com/watch?v=HyHNuVaZJ-k) + * @param callback listener that will be called-back when events occur (check {@link StreamExtractorWorker.OnStreamInfoReceivedListener}) + */ + public StreamExtractorWorker(Context context, int serviceId, String videoUrl, OnStreamInfoReceivedListener callback) { + super(context, videoUrl, serviceId); + this.callback = callback; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + this.callback = null; + this.streamInfo = null; + } + + @Override + protected void doWork(int serviceId, String url) throws Exception { + StreamExtractor streamExtractor = getService().getExtractorInstance(url); + streamInfo = StreamInfo.getVideoInfo(streamExtractor); + + if (streamInfo != null && !streamInfo.errors.isEmpty()) handleErrorsDuringExtraction(streamInfo.errors, ErrorActivity.REQUESTED_STREAM); + + if (callback != null && streamInfo != null && !isInterrupted()) getHandler().post(new Runnable() { + @Override + public void run() { + if (isInterrupted() || callback == null) return; + + callback.onReceive(streamInfo); + onDestroy(); + } + }); + + } + + @Override + protected void handleException(final Exception exception, int serviceId, String url) { + if (exception instanceof ReCaptchaException) { + if (callback != null) getHandler().post(new Runnable() { + @Override + public void run() { + callback.onReCaptchaException(); + } + }); + } else if (exception instanceof IOException) { + if (callback != null) getHandler().post(new Runnable() { + @Override + public void run() { + callback.onError(R.string.network_error); + } + }); + } else if (exception instanceof YoutubeStreamExtractor.GemaException) { + if (callback != null) getHandler().post(new Runnable() { + @Override + public void run() { + callback.onBlockedByGemaError(); + } + }); + } else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) { + if (callback != null) getHandler().post(new Runnable() { + @Override + public void run() { + callback.onContentErrorWithMessage(R.string.live_streams_not_supported); + } + }); + } else if (exception instanceof StreamExtractor.ContentNotAvailableException) { + if (callback != null) getHandler().post(new Runnable() { + @Override + public void run() { + callback.onContentError(); + } + }); + } else if (exception instanceof YoutubeStreamExtractor.DecryptException) { + // custom service related exceptions + ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.youtube_signature_decryption_error)); + finishIfActivity(); + } else if (exception instanceof StreamInfo.StreamExctractException) { + if (!streamInfo.errors.isEmpty()) { + // !!! if this case ever kicks in someone gets kicked out !!! + ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.could_not_get_stream)); + } else { + ErrorActivity.reportError(getHandler(), getContext(), streamInfo.errors, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.could_not_get_stream)); + } + finishIfActivity(); + } else if (exception instanceof ParsingException) { + ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.parsing_error)); + finishIfActivity(); + } else { + ErrorActivity.reportError(getHandler(), getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(ErrorActivity.REQUESTED_STREAM, getServiceName(), url, R.string.general_error)); + finishIfActivity(); + } + + } + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 31c1c587a..67292ab7d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,15 +1,16 @@ - + android:orientation="vertical" + tools:context="org.schabi.newpipe.MainActivity"> - + android:layout_height="match_parent"/> diff --git a/app/src/main/res/layout/activity_channel.xml b/app/src/main/res/layout/fragment_channel.xml similarity index 50% rename from app/src/main/res/layout/activity_channel.xml rename to app/src/main/res/layout/fragment_channel.xml index 5a3257a95..78388ee76 100644 --- a/app/src/main/res/layout/activity_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -3,24 +3,22 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" - android:title="Channel"> + android:title="@string/channel"> + android:scrollbars="vertical" + tools:listitem="@layout/stream_item"/> - - - + diff --git a/app/src/main/res/layout/fragment_searchinfoitem.xml b/app/src/main/res/layout/fragment_search.xml similarity index 83% rename from app/src/main/res/layout/fragment_searchinfoitem.xml rename to app/src/main/res/layout/fragment_search.xml index 09064076d..2ed0ea0f7 100644 --- a/app/src/main/res/layout/fragment_searchinfoitem.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent" android:layout_width="match_parent" android:name="org.schabi.newpipe.SearchInfoItemFragment" - tools:context=".search_fragment.SearchInfoItemFragment"> + tools:context=".fragments.search.SearchFragment"> @@ -17,10 +17,11 @@ tools:listitem="@layout/stream_item" android:scrollbars="vertical"/> - + android:visibility="gone" + tools:visibility="visible"/> diff --git a/app/src/main/res/layout/activity_videoitem_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml similarity index 96% rename from app/src/main/res/layout/activity_videoitem_detail.xml rename to app/src/main/res/layout/fragment_video_detail.xml index 0763f7ee6..4b34bcb49 100644 --- a/app/src/main/res/layout/activity_videoitem_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -17,6 +17,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + + + android:layout_height="wrap_content" + android:layout_below="@+id/detail_thumbnail_root_layout" + android:background="?android:windowBackground"> + + + + + + + diff --git a/app/src/main/res/layout/main_bg.xml b/app/src/main/res/layout/main_bg.xml index 00ab5916a..7c8c877d5 100644 --- a/app/src/main/res/layout/main_bg.xml +++ b/app/src/main/res/layout/main_bg.xml @@ -4,7 +4,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/mainBG" - tools:context=".detail.VideoItemDetailActivity"> + tools:context=".fragments.detail.VideoDetailFragment"> + android:orderInCategory="980" + android:title="@string/downloads" + app:showAsAction="never"/> + android:orderInCategory="990" + android:title="@string/settings" + app:showAsAction="never"/> \ No newline at end of file diff --git a/app/src/main/res/menu/menu_channel.xml b/app/src/main/res/menu/menu_channel.xml index 58f0f7750..c371699be 100644 --- a/app/src/main/res/menu/menu_channel.xml +++ b/app/src/main/res/menu/menu_channel.xml @@ -1,7 +1,7 @@

+ tools:context="org.schabi.newpipe.fragments.channel.ChannelFragment"> - - diff --git a/app/src/main/res/menu/search_menu.xml b/app/src/main/res/menu/search_menu.xml index 027df6668..d3df69b25 100644 --- a/app/src/main/res/menu/search_menu.xml +++ b/app/src/main/res/menu/search_menu.xml @@ -8,7 +8,8 @@ app:actionViewClass="android.support.v7.widget.SearchView" /> + android:checkableBehavior="single" + android:orderInCategory="999"> diff --git a/app/src/main/res/menu/videoitem_detail.xml b/app/src/main/res/menu/videoitem_detail.xml index c4f2d5fb1..6143ee974 100644 --- a/app/src/main/res/menu/videoitem_detail.xml +++ b/app/src/main/res/menu/videoitem_detail.xml @@ -29,14 +29,4 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cdf20f441..c4340a093 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,6 +158,7 @@ Old build in Mediaframework player. videos subscriber + subscribers Subscribe views K From 06fafc247e9e7c2cf81d2ede67011c22743c691c Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 11 Apr 2017 22:49:18 +0200 Subject: [PATCH 2/3] move to version 0.9.2 of NewPipeExtractor --- app/src/main/java/org/schabi/newpipe/extractor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/extractor b/app/src/main/java/org/schabi/newpipe/extractor index 6ab3dc876..b587d175b 160000 --- a/app/src/main/java/org/schabi/newpipe/extractor +++ b/app/src/main/java/org/schabi/newpipe/extractor @@ -1 +1 @@ -Subproject commit 6ab3dc876ebab4ed32f4ae60d3d04d000a7ea0e8 +Subproject commit b587d175bb9cd7e84b9bdb9b594db24e654ec7ae From e5bf98a74155d5393cefe821df676c555424bf53 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 11 Apr 2017 22:51:04 +0200 Subject: [PATCH 3/3] move on to version 0.9.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1245f5c46..da85fbecc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.schabi.newpipe" minSdkVersion 15 targetSdkVersion 25 - versionCode 28 - versionName "0.9.1" + versionCode 29 + versionName "0.9.2" } buildTypes { release {