From 6d74038866522aa8b497a91248eee6a722cb2e20 Mon Sep 17 00:00:00 2001 From: Coffeemakr Date: Thu, 15 Jun 2017 16:26:48 +0200 Subject: [PATCH 01/13] Improve speed * Replace relative layouts and use Recycler view * Handle HTML in background --- .../java/org/schabi/newpipe/MainActivity.java | 4 +- .../fragments/channel/ChannelFragment.java | 2 +- .../fragments/detail/VideoDetailFragment.java | 235 +++++++---- .../fragments/search/SearchFragment.java | 2 +- .../info_list/ChannelInfoItemHolder.java | 6 +- .../newpipe/info_list/InfoItemBuilder.java | 169 ++++---- .../newpipe/info_list/InfoListAdapter.java | 41 +- .../info_list/StreamInfoItemHolder.java | 6 +- .../schabi/newpipe/util/AnimationUtils.java | 4 +- app/src/main/res/layout/activity_main.xml | 4 +- app/src/main/res/layout/channel_item.xml | 83 ++-- app/src/main/res/layout/error_retry.xml | 8 +- app/src/main/res/layout/fragment_channel.xml | 13 +- app/src/main/res/layout/fragment_search.xml | 11 +- .../main/res/layout/fragment_video_detail.xml | 368 ++++++++---------- app/src/main/res/layout/stream_item.xml | 84 ++-- .../main/res/layout/toolbar_search_layout.xml | 9 +- app/src/main/res/values/dimens.xml | 4 + 18 files changed, 546 insertions(+), 507 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index f747ca9a5..50ce8c55a 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -102,7 +102,9 @@ public class MainActivity extends AppCompatActivity { if (DEBUG) Log.d(TAG, "onBackPressed() called"); Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder); - if (fragment instanceof VideoDetailFragment) if (((VideoDetailFragment) fragment).onActivityBackPressed()) return; + if (fragment instanceof VideoDetailFragment) { + if (((VideoDetailFragment) fragment).onActivityBackPressed()) return; + } super.onBackPressed(); 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 index 9a1cfe1a7..08fb09cbf 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/channel/ChannelFragment.java @@ -213,7 +213,7 @@ public class ChannelFragment extends BaseFragment implements ChannelExtractorWor channelVideosList.setLayoutManager(new LinearLayoutManager(activity)); if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(activity, rootView); + infoListAdapter = new InfoListAdapter(activity); if (savedInstanceState != null) { //noinspection unchecked ArrayList serializable = (ArrayList) savedInstanceState.getSerializable(INFO_LIST_KEY); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 2721eefc9..f568f9f0f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -1,12 +1,17 @@ package org.schabi.newpipe.fragments.detail; import android.app.Activity; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; import android.preference.PreferenceManager; import android.support.annotation.FloatRange; import android.support.annotation.NonNull; @@ -14,7 +19,10 @@ import android.support.v4.content.ContextCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.text.Html; +import android.text.Spanned; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.Log; @@ -26,7 +34,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -43,14 +51,15 @@ import org.schabi.newpipe.ImageErrorLoadingListener; import org.schabi.newpipe.R; import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.download.DownloadDialog; -import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.MediaFormat; 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.StreamInfoItem; import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.fragments.BaseFragment; import org.schabi.newpipe.info_list.InfoItemBuilder; +import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PlayVideoActivity; import org.schabi.newpipe.player.PopupVideoPlayer; @@ -88,7 +97,6 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor private ArrayList sortedStreamVideosList; private ActionBarHandler actionBarHandler; - private InfoItemBuilder infoItemBuilder = null; private StreamInfo currentStreamInfo = null; private StreamExtractorWorker curExtractorWorker; @@ -112,9 +120,9 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor private Spinner spinnerToolbar; private ParallaxScrollView parallaxScrollRootView; - private RelativeLayout contentRootLayoutHiding; + private LinearLayout contentRootLayoutHiding; - private Button thumbnailBackgroundButton; + private View thumbnailBackgroundButton; private ImageView thumbnailImageView; private ImageView thumbnailPlayButton; @@ -126,12 +134,11 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor private TextView detailControlsBackground; private TextView detailControlsPopup; - private RelativeLayout videoDescriptionRootLayout; + private LinearLayout videoDescriptionRootLayout; private TextView videoUploadDateView; private TextView videoDescriptionView; private View uploaderRootLayout; - private Button uploaderButton; private TextView uploaderTextView; private ImageView uploaderThumb; @@ -142,9 +149,12 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor private TextView thumbsDisabledTextView; private TextView nextStreamTitle; - private RelativeLayout relatedStreamRootLayout; - private LinearLayout relatedStreamsView; + private LinearLayout relatedStreamRootLayout; private ImageButton relatedStreamExpandButton; + private Handler uiHandler; + private InfoListAdapter relatedStreamsAdapter; + private Handler backgroundHandler; + private HandlerThread backgroundHandlerThread; /*////////////////////////////////////////////////////////////////////////*/ @@ -194,6 +204,17 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor thousand = getString(R.string.short_thousand); million = getString(R.string.short_million); billion = getString(R.string.short_billion); + + if(uiHandler == null) { + uiHandler = new Handler(Looper.getMainLooper(), new UICallback()); + } + if(backgroundHandler == null) { + HandlerThread handlerThread = new HandlerThread("VideoDetailFragment-BG"); + handlerThread.start(); + backgroundHandlerThread = handlerThread; + backgroundHandler = new Handler(handlerThread.getLooper(), new BackgroundCallback(uiHandler, getContext())); + } + relatedStreamsAdapter = new InfoListAdapter(getActivity()); } @Override @@ -241,6 +262,11 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor @Override public void onDestroy() { super.onDestroy(); + if(backgroundHandlerThread != null) { + backgroundHandlerThread.quit(); + } + backgroundHandlerThread = null; + backgroundHandler = null; PreferenceManager.getDefaultSharedPreferences(activity).unregisterOnSharedPreferenceChangeListener(this); } @@ -248,7 +274,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor public void onDestroyView() { if (DEBUG) Log.d(TAG, "onDestroyView() called"); thumbnailImageView.setImageBitmap(null); - relatedStreamsView.removeAllViews(); + relatedStreamsAdapter.clearStreamItemList(); spinnerToolbar.setOnItemSelectedListener(null); spinnerToolbar = null; @@ -272,7 +298,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor videoUploadDateView = null; videoDescriptionView = null; - uploaderButton = null; + uploaderRootLayout = null; uploaderTextView = null; uploaderThumb = null; @@ -284,7 +310,6 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor nextStreamTitle = null; relatedStreamRootLayout = null; - relatedStreamsView = null; relatedStreamExpandButton = null; super.onDestroyView(); @@ -299,7 +324,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor outState.putSerializable(STACK_KEY, stack); int nextCount = currentStreamInfo != null && currentStreamInfo.next_video != null ? 2 : 0; - if (relatedStreamsView != null && relatedStreamsView.getChildCount() > INITIAL_RELATED_VIDEOS + nextCount) { + if (relatedStreamsAdapter != null && relatedStreamsAdapter.getItemCount() > INITIAL_RELATED_VIDEOS + nextCount) { outState.putSerializable(WAS_RELATED_EXPANDED_KEY, true); } @@ -353,10 +378,14 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor case R.id.detail_controls_popup: openInPopup(); break; - case R.id.detail_uploader_button: - NavigationHelper.openChannelFragment(getFragmentManager(), currentStreamInfo.service_id, currentStreamInfo.channel_url, currentStreamInfo.uploader); + case R.id.detail_uploader_root_layout: + if(currentStreamInfo.channel_url == null || currentStreamInfo.channel_url.isEmpty()) { + Log.w(TAG, "Can't open channel because we got no channel URL"); + } else { + NavigationHelper.openChannelFragment(getFragmentManager(), currentStreamInfo.service_id, currentStreamInfo.channel_url, currentStreamInfo.uploader); + } break; - case R.id.detail_thumbnail_background_button: + case R.id.detail_thumbnail_root_layout: playVideo(currentStreamInfo); break; case R.id.detail_title_root_layout: @@ -458,19 +487,13 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor int nextCount = info.next_video != null ? 2 : 0; int initialCount = INITIAL_RELATED_VIDEOS + nextCount; - if (relatedStreamsView.getChildCount() > initialCount) { - relatedStreamsView.removeViews(initialCount, relatedStreamsView.getChildCount() - (initialCount)); + if (relatedStreamsAdapter.getItemCount() > initialCount) { + relatedStreamsAdapter.removeItemRange(initialCount, relatedStreamsAdapter.getItemCount()); relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, getResourceIdFromAttr(R.attr.expand))); - return; + } else { + relatedStreamsAdapter.addInfoItemList(info.related_streams.subList(INITIAL_RELATED_VIDEOS, info.related_streams.size())); + relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, getResourceIdFromAttr(R.attr.collapse))); } - - //Log.d(TAG, "toggleExpandRelatedVideos() called with: info = [" + info + "], from = [" + INITIAL_RELATED_VIDEOS + "]"); - for (int i = INITIAL_RELATED_VIDEOS; i < info.related_streams.size(); i++) { - InfoItem item = info.related_streams.get(i); - //Log.d(TAG, "i = " + i); - relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); - } - relatedStreamExpandButton.setImageDrawable(ContextCompat.getDrawable(activity, getResourceIdFromAttr(R.attr.collapse))); } /*////////////////////////////////////////////////////////////////////////// @@ -484,12 +507,11 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor parallaxScrollRootView = (ParallaxScrollView) rootView.findViewById(R.id.detail_main_content); - //thumbnailRootLayout = (RelativeLayout) rootView.findViewById(R.id.detail_thumbnail_root_layout); - thumbnailBackgroundButton = (Button) rootView.findViewById(R.id.detail_thumbnail_background_button); + thumbnailBackgroundButton = rootView.findViewById(R.id.detail_thumbnail_root_layout); thumbnailImageView = (ImageView) rootView.findViewById(R.id.detail_thumbnail_image_view); thumbnailPlayButton = (ImageView) rootView.findViewById(R.id.detail_thumbnail_play_button); - contentRootLayoutHiding = (RelativeLayout) rootView.findViewById(R.id.detail_content_root_hiding); + contentRootLayoutHiding = (LinearLayout) rootView.findViewById(R.id.detail_content_root_hiding); videoTitleRoot = rootView.findViewById(R.id.detail_title_root_layout); videoTitleTextView = (TextView) rootView.findViewById(R.id.detail_video_title_view); @@ -499,7 +521,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor detailControlsBackground = (TextView) rootView.findViewById(R.id.detail_controls_background); detailControlsPopup = (TextView) rootView.findViewById(R.id.detail_controls_popup); - videoDescriptionRootLayout = (RelativeLayout) rootView.findViewById(R.id.detail_description_root_layout); + videoDescriptionRootLayout = (LinearLayout) 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); @@ -511,26 +533,28 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor thumbsDisabledTextView = (TextView) rootView.findViewById(R.id.detail_thumbs_disabled_view); uploaderRootLayout = 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) rootView.findViewById(R.id.detail_related_streams_root_layout); + relatedStreamRootLayout = (LinearLayout) 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); + RecyclerView relatedStreamsView = (RecyclerView) rootView.findViewById(R.id.detail_related_streams_view); + LinearLayoutManager llm = new LinearLayoutManager(rootView.getContext()); + llm.setOrientation(LinearLayoutManager.VERTICAL); + relatedStreamsView.setLayoutManager(llm); + relatedStreamsView.setAdapter(relatedStreamsAdapter); + relatedStreamExpandButton = ((ImageButton) rootView.findViewById(R.id.detail_related_streams_expand)); actionBarHandler = new ActionBarHandler(activity); videoDescriptionView.setMovementMethod(LinkMovementMethod.getInstance()); - infoItemBuilder = new InfoItemBuilder(activity, rootView.findViewById(android.R.id.content)); - setHeightThumbnail(); } protected void initListeners() { super.initListeners(); - infoItemBuilder.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { + relatedStreamsAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() { @Override public void selected(int serviceId, String url, String title) { //NavigationHelper.openVideoDetail(activity, url, serviceId); @@ -539,7 +563,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor }); videoTitleRoot.setOnClickListener(this); - uploaderButton.setOnClickListener(this); + uploaderRootLayout.setOnClickListener(this); thumbnailBackgroundButton.setOnClickListener(this); detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); @@ -563,24 +587,18 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } private void initRelatedVideos(StreamInfo info) { - if (relatedStreamsView.getChildCount() > 0) relatedStreamsView.removeAllViews(); + relatedStreamsAdapter.clearStreamItemList(); if (info.next_video != null && showRelatedStreams) { nextStreamTitle.setVisibility(View.VISIBLE); - relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, info.next_video)); - relatedStreamsView.addView(getSeparatorView()); + relatedStreamsAdapter.addInfoItem(info.next_video); relatedStreamRootLayout.setVisibility(View.VISIBLE); } else nextStreamTitle.setVisibility(View.GONE); if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) { //long first = System.nanoTime(), each; int to = info.related_streams.size() >= INITIAL_RELATED_VIDEOS ? INITIAL_RELATED_VIDEOS : info.related_streams.size(); - for (int i = 0; i < to; i++) { - InfoItem item = info.related_streams.get(i); - //each = System.nanoTime(); - relatedStreamsView.addView(infoItemBuilder.buildView(relatedStreamsView, item)); - //if (DEBUG) Log.d(TAG, "each took " + ((System.nanoTime() - each) / 1000000L) + "ms"); - } + relatedStreamsAdapter.addInfoItemList(info.related_streams.subList(0, to)); //if (DEBUG) Log.d(TAG, "Total time " + ((System.nanoTime() - first) / 1000000L) + "ms"); relatedStreamRootLayout.setVisibility(View.VISIBLE); @@ -739,8 +757,17 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor // Get url from the new top StackItem peek = stack.peek(); - if (peek.getInfo() != null) selectAndHandleInfo(peek.getInfo()); - else selectAndLoadVideo(0, peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : ""); + if (peek.getInfo() != null) { + final StreamInfo streamInfo = peek.getInfo(); + uiHandler.post(new Runnable() { + @Override + public void run() { + selectAndHandleInfo(streamInfo); + } + }); + } else { + selectAndLoadVideo(0, peek.getUrl(), !TextUtils.isEmpty(peek.getTitle()) ? peek.getTitle() : ""); + } return true; } @@ -848,7 +875,7 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor } } - private void handleStreamInfo(@NonNull StreamInfo info, boolean fromNetwork) { + private void handleStreamInfo(@NonNull final StreamInfo info, boolean fromNetwork) { if (DEBUG) Log.d(TAG, "handleStreamInfo() called with: info = [" + info + "]"); currentStreamInfo = info; selectVideo(info.service_id, info.webpage_url, info.title); @@ -862,7 +889,6 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor if (!TextUtils.isEmpty(info.uploader)) uploaderTextView.setText(info.uploader); uploaderTextView.setVisibility(!TextUtils.isEmpty(info.uploader) ? View.VISIBLE : View.GONE); - uploaderButton.setVisibility(!TextUtils.isEmpty(info.channel_url) ? View.VISIBLE : View.GONE); uploaderThumb.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy)); if (info.view_count >= 0) videoCountView.setText(Localization.localizeViewCount(info.view_count, activity)); @@ -887,14 +913,8 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor thumbsUpImageView.setVisibility(info.like_count >= 0 ? View.VISIBLE : View.GONE); } - if (!TextUtils.isEmpty(info.upload_date)) videoUploadDateView.setText(Localization.localizeDate(info.upload_date, activity)); - videoUploadDateView.setVisibility(!TextUtils.isEmpty(info.upload_date) ? View.VISIBLE : View.GONE); - - if (!TextUtils.isEmpty(info.description)) { //noinspection deprecation - videoDescriptionView.setText(Build.VERSION.SDK_INT >= 24 ? Html.fromHtml(info.description, 0) : Html.fromHtml(info.description)); - } - videoDescriptionView.setVisibility(!TextUtils.isEmpty(info.description) ? View.VISIBLE : View.GONE); + videoDescriptionView.setVisibility(View.GONE); videoDescriptionRootLayout.setVisibility(View.GONE); videoTitleToggleArrow.setImageResource(R.drawable.arrow_down); videoTitleToggleArrow.setVisibility(View.VISIBLE); @@ -903,14 +923,36 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor animateView(spinnerToolbar, true, 500); setupActionBarHandler(info); initThumbnailViews(info); - initRelatedVideos(info); - if (wasRelatedStreamsExpanded) { - toggleExpandRelatedVideos(currentStreamInfo); - wasRelatedStreamsExpanded = false; - } - + uiHandler.post(new Runnable() { + @Override + public void run() { + initRelatedVideos(info); + if (wasRelatedStreamsExpanded) { + toggleExpandRelatedVideos(currentStreamInfo); + wasRelatedStreamsExpanded = false; + } + } + }); setTitleToUrl(info.webpage_url, info.title); setStreamInfoToUrl(info.webpage_url, info); + + + prepareDescription(info.description); + prepareUploadDate(info.upload_date); + } + private void prepareUploadDate(final String uploadDate) { + // Hide until date is prepared or forever if no date is supplied + videoUploadDateView.setVisibility(View.GONE); + if (!TextUtils.isEmpty(uploadDate)) { + backgroundHandler.sendMessage(Message.obtain(backgroundHandler, BackgroundCallback.MESSAGE_UPLOADER_DATE, uploadDate)); + } + } + + private void prepareDescription(final String descriptionHtml) { + // Send the unparsed description to the handler as a message + if (!TextUtils.isEmpty(descriptionHtml)) { + backgroundHandler.sendMessage(Message.obtain(backgroundHandler, BackgroundCallback.MESSAGE_DESCRIPTION, descriptionHtml)); + } } public void playVideo(StreamInfo info) { @@ -987,10 +1029,8 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor int height = isPortrait ? (int) (getResources().getDisplayMetrics().widthPixels / (16.0f / 9.0f)) : (int) (getResources().getDisplayMetrics().heightPixels / 2f); thumbnailImageView.setScaleType(isPortrait ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.FIT_CENTER); - thumbnailImageView.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); + thumbnailImageView.setLayoutParams(new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); thumbnailImageView.setMinimumHeight(height); - thumbnailBackgroundButton.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height)); - thumbnailBackgroundButton.setMinimumHeight(height); } public String getShortCount(Long viewCount) { @@ -1126,4 +1166,65 @@ public class VideoDetailFragment extends BaseFragment implements StreamExtractor public void onUnrecoverableError(Exception exception) { activity.finish(); } + + private static class BackgroundCallback implements Handler.Callback { + private static final int MESSAGE_DESCRIPTION = 1; + public static final int MESSAGE_UPLOADER_DATE = 2; + private final Handler uiHandler; + private final Context context; + + BackgroundCallback(Handler uiHandler, Context context) { + this.uiHandler = uiHandler; + this.context = context; + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_DESCRIPTION: + handleDescription((String) msg.obj); + return true; + case MESSAGE_UPLOADER_DATE: + handleUploadDate((String) msg.obj); + return true; + } + return false; + } + + private void handleUploadDate(String uploadDate) { + String localizedDate = Localization.localizeDate(uploadDate, context); + uiHandler.sendMessage(Message.obtain(uiHandler, MESSAGE_UPLOADER_DATE, localizedDate)); + } + + private void handleDescription(String description) { + Spanned parsedDescription; + if (TextUtils.isEmpty(description)) { + return; + } + if (Build.VERSION.SDK_INT >= 24) { + parsedDescription = Html.fromHtml(description, 0); + } else { + //noinspection deprecation + parsedDescription = Html.fromHtml(description); + } + uiHandler.sendMessage(Message.obtain(uiHandler, MESSAGE_DESCRIPTION, parsedDescription)); + } + } + + private class UICallback implements Handler.Callback { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case BackgroundCallback.MESSAGE_DESCRIPTION: + videoDescriptionView.setText((Spanned) msg.obj); + videoDescriptionView.setVisibility(View.VISIBLE); + return true; + case BackgroundCallback.MESSAGE_UPLOADER_DATE: + videoUploadDateView.setText((String) msg.obj); + videoUploadDateView.setVisibility(View.VISIBLE); + return true; + } + return false; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java index 3e04b94fd..cfc45c9ba 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/search/SearchFragment.java @@ -212,7 +212,7 @@ public class SearchFragment extends BaseFragment implements SuggestionWorker.OnS resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity)); if (infoListAdapter == null) { - infoListAdapter = new InfoListAdapter(getActivity(), getActivity().findViewById(android.R.id.content)); + infoListAdapter = new InfoListAdapter(getActivity()); if (savedInstanceState != null) { //noinspection unchecked ArrayList serializable = (ArrayList) savedInstanceState.getSerializable(INFO_LIST_KEY); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java index 29eaafcd9..5543907e5 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/ChannelInfoItemHolder.java @@ -31,8 +31,7 @@ import de.hdodenhof.circleimageview.CircleImageView; public class ChannelInfoItemHolder extends InfoItemHolder { public final CircleImageView itemThumbnailView; public final TextView itemChannelTitleView; - public final TextView itemSubscriberCountView; - public final TextView itemVideoCountView; + public final TextView itemAdditionalDetailView; public final TextView itemChannelDescriptionView; public final View itemRoot; @@ -42,8 +41,7 @@ public class ChannelInfoItemHolder extends InfoItemHolder { itemRoot = v.findViewById(R.id.itemRoot); itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView); itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView); - itemSubscriberCountView = (TextView) v.findViewById(R.id.itemSubscriberCountView); - itemVideoCountView = (TextView) v.findViewById(R.id.itemVideoCountView); + itemAdditionalDetailView = (TextView) v.findViewById(R.id.itemAdditionalDetails); itemChannelDescriptionView = (TextView) v.findViewById(R.id.itemChannelDescriptionView); } 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 f83d954a2..9cf27bb6f 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 @@ -17,6 +17,8 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.stream_info.StreamInfoItem; +import java.util.Locale; + /** * Created by Christian Schabesberger on 26.09.16. *

@@ -54,18 +56,30 @@ public class InfoItemBuilder { void selected(int serviceId, String url, String title); } - private Context mContext = null; - private LayoutInflater inflater; - private View rootView = null; private ImageLoader imageLoader = ImageLoader.getInstance(); - private DisplayImageOptions displayImageOptions = - new DisplayImageOptions.Builder().cacheInMemory(true).build(); + private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS = + new DisplayImageOptions.Builder() + .cacheInMemory(true) + .build(); + private static final DisplayImageOptions DISPLAY_STREAM_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(DISPLAY_IMAGE_OPTIONS) + .showImageOnFail(R.drawable.dummy_thumbnail) + .showImageForEmptyUri(R.drawable.dummy_thumbnail) + .showImageOnLoading(R.drawable.dummy_thumbnail) + .build(); + + private static final DisplayImageOptions DISPLAY_CHANNEL_THUMBNAIL_OPTIONS = + new DisplayImageOptions.Builder() + .cloneFrom(DISPLAY_IMAGE_OPTIONS) + .showImageOnLoading(R.drawable.buddy_channel_item) + .showImageForEmptyUri(R.drawable.buddy_channel_item) + .showImageOnFail(R.drawable.buddy_channel_item) + .build(); private OnInfoItemSelectedListener onStreamInfoItemSelectedListener; private OnInfoItemSelectedListener onChannelInfoItemSelectedListener; - public InfoItemBuilder(Context context, View rootView) { - mContext = context; - this.rootView = rootView; + public InfoItemBuilder(Context context) { viewsS = context.getString(R.string.views); videosS = context.getString(R.string.videos); subsS = context.getString(R.string.subscriber); @@ -73,7 +87,6 @@ public class InfoItemBuilder { thousand = context.getString(R.string.short_thousand); million = context.getString(R.string.short_million); billion = context.getString(R.string.short_billion); - inflater = LayoutInflater.from(context); } public void setOnStreamInfoItemSelectedListener( @@ -104,27 +117,19 @@ public class InfoItemBuilder { } } - public View buildView(ViewGroup parent, final InfoItem info) { - View itemView = null; - InfoItemHolder holder = null; - switch (info.infoType()) { - case STREAM: - //long start = System.nanoTime(); - itemView = inflater.inflate(R.layout.stream_item, parent, false); - //Log.d(TAG, "time to inflate: " + ((System.nanoTime() - start) / 1000000L) + "ms"); - holder = new StreamInfoItemHolder(itemView); - break; - case CHANNEL: - itemView = inflater.inflate(R.layout.channel_item, parent, false); - holder = new ChannelInfoItemHolder(itemView); - break; - case PLAYLIST: - Log.e(TAG, "Not yet implemented"); - default: - Log.e(TAG, "Trollolo"); + private String getStreamInfoDetailLine(final StreamInfoItem info) { + String viewsAndDate = ""; + if(info.view_count >= 0) { + viewsAndDate = shortViewCount(info.view_count); } - buildByHolder(holder, info); - return itemView; + if(!TextUtils.isEmpty(info.upload_date)) { + if(viewsAndDate.isEmpty()) { + viewsAndDate = info.upload_date; + } else { + viewsAndDate += " • " + info.upload_date; + } + } + return viewsAndDate; } private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) { @@ -146,46 +151,59 @@ public class InfoItemBuilder { holder.itemDurationView.setVisibility(View.GONE); } } - if (info.view_count >= 0) { - holder.itemViewCountView.setText(shortViewCount(info.view_count)); - } else { - holder.itemViewCountView.setVisibility(View.GONE); - } - if (!TextUtils.isEmpty(info.upload_date)) holder.itemUploadDateView.setText(info.upload_date + " • "); - holder.itemThumbnailView.setImageResource(R.drawable.dummy_thumbnail); - if (!TextUtils.isEmpty(info.thumbnail_url)) { - imageLoader.displayImage(info.thumbnail_url, - holder.itemThumbnailView, displayImageOptions, - new ImageErrorLoadingListener(mContext, rootView, info.service_id)); - } + holder.itemAdditionalDetails.setText(getStreamInfoDetailLine(info)); + + // Default thumbnail is shown on error, while loading and if the url is empty + imageLoader.displayImage(info.thumbnail_url, + holder.itemThumbnailView, + DISPLAY_STREAM_THUMBNAIL_OPTIONS, + new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.service_id)); + holder.itemRoot.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onStreamInfoItemSelectedListener.selected(info.service_id, info.webpage_url, info.getTitle()); + if(onStreamInfoItemSelectedListener != null) { + onStreamInfoItemSelectedListener.selected(info.service_id, info.webpage_url, info.getTitle()); + } } }); } + private String getChannelInfoDetailLine(final ChannelInfoItem info) { + String details = ""; + if(info.subscriberCount >= 0) { + details = shortSubscriber(info.subscriberCount); + } + if(info.videoAmount >= 0) { + String formattedVideoAmount = info.videoAmount + " " + videosS; + if(!details.isEmpty()) { + details += " • " + formattedVideoAmount; + } else { + details = formattedVideoAmount; + } + } + return details; + } + private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) { if (!TextUtils.isEmpty(info.getTitle())) holder.itemChannelTitleView.setText(info.getTitle()); - holder.itemSubscriberCountView.setText(shortSubscriber(info.subscriberCount) + " • "); - holder.itemVideoCountView.setText(info.videoAmount + " " + videosS); + holder.itemAdditionalDetailView.setText(getChannelInfoDetailLine(info)); if (!TextUtils.isEmpty(info.description)) holder.itemChannelDescriptionView.setText(info.description); - holder.itemThumbnailView.setImageResource(R.drawable.buddy_channel_item); - if (!TextUtils.isEmpty(info.thumbnailUrl)) { - imageLoader.displayImage(info.thumbnailUrl, - holder.itemThumbnailView, - displayImageOptions, - new ImageErrorLoadingListener(mContext, rootView, info.serviceId)); - } + imageLoader.displayImage(info.thumbnailUrl, + holder.itemThumbnailView, + DISPLAY_CHANNEL_THUMBNAIL_OPTIONS, + new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.serviceId)); + holder.itemRoot.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName); + if(onStreamInfoItemSelectedListener != null) { + onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName); + } } }); } @@ -218,7 +236,10 @@ public class InfoItemBuilder { } public static String getDurationString(int duration) { - String output = ""; + if(duration < 0) { + duration = 0; + } + String output; int days = duration / (24 * 60 * 60); /* greater than a day */ duration %= (24 * 60 * 60); int hours = duration / (60 * 60); /* greater than an hour */ @@ -228,46 +249,12 @@ public class InfoItemBuilder { //handle days if (days > 0) { - output = Integer.toString(days) + ":"; - } - // handle hours - if (hours > 0 || !output.isEmpty()) { - if (hours > 0) { - if (hours >= 10 || output.isEmpty()) { - output += Integer.toString(hours); - } else { - output += "0" + Integer.toString(hours); - } - } else { - output += "00"; - } - output += ":"; - } - //handle minutes - if (minutes > 0 || !output.isEmpty()) { - if (minutes > 0) { - if (minutes >= 10 || output.isEmpty()) { - output += Integer.toString(minutes); - } else { - output += "0" + Integer.toString(minutes); - } - } else { - output += "00"; - } - output += ":"; - } - - //handle seconds - if (output.isEmpty()) { - output += "0:"; - } - - if (seconds >= 10) { - output += Integer.toString(seconds); + output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds); + } else if(hours > 0) { + output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds); } else { - output += "0" + Integer.toString(seconds); + output = String.format(Locale.US, "%d:%02d", minutes, seconds); } - return output; } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java index e17e2aaac..eb5ead13c 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoListAdapter.java @@ -11,6 +11,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -37,7 +38,7 @@ public class InfoListAdapter extends RecyclerView.Adapter infoItemList; + private final InfoItemList infoItemList; private boolean showFooter = false; private View header = null; private View footer = null; @@ -55,9 +56,9 @@ public class InfoListAdapter extends RecyclerView.Adapter(); + public InfoListAdapter(Activity a) { + infoItemBuilder = new InfoItemBuilder(a); + infoItemList = new InfoItemList(); } public void setOnStreamInfoItemSelectedListener @@ -70,14 +71,26 @@ public class InfoListAdapter extends RecyclerView.Adapter data) { + public void addInfoItemList(Collection data) { if(data != null) { + int sizeBefore = infoItemList.size(); infoItemList.addAll(data); - notifyDataSetChanged(); + notifyItemRangeInserted(sizeBefore, data.size()); } } + public void addInfoItem(InfoItem infoItem) { + if(infoItem == null) { + throw new NullPointerException("infoItem is null"); + } + infoItemList.add(infoItem); + notifyItemInserted(infoItemList.size()); + } + public void clearStreamItemList() { + if(infoItemList.isEmpty()) { + return; + } infoItemList.clear(); notifyDataSetChanged(); } @@ -96,6 +109,15 @@ public class InfoListAdapter extends RecyclerView.Adapter { + @Override + protected void removeRange(int fromIndex, int toIndex) { + super.removeRange(fromIndex, toIndex); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java index a527cd300..0b0d48a12 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamInfoItemHolder.java @@ -33,8 +33,7 @@ public class StreamInfoItemHolder extends InfoItemHolder { public final TextView itemVideoTitleView, itemUploaderView, itemDurationView, - itemUploadDateView, - itemViewCountView; + itemAdditionalDetails; public final View itemRoot; public StreamInfoItemHolder(View v) { @@ -44,8 +43,7 @@ public class StreamInfoItemHolder extends InfoItemHolder { itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView); itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView); itemDurationView = (TextView) v.findViewById(R.id.itemDurationView); - itemUploadDateView = (TextView) v.findViewById(R.id.itemUploadDateView); - itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView); + itemAdditionalDetails = (TextView) v.findViewById(R.id.itemAdditionalDetails); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index c37eaa560..7fb64213a 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -55,14 +55,14 @@ public class AnimationUtils { view.animate().setListener(null).cancel(); view.setVisibility(View.VISIBLE); view.setAlpha(1f); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) view.post(execOnEnd); return; } else if ((view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) && !enterOrExit) { if (DEBUG) Log.d(TAG, "animateView() view was already gone > view = [" + view + "]"); view.animate().setListener(null).cancel(); view.setVisibility(View.GONE); view.setAlpha(0f); - if (execOnEnd != null) execOnEnd.run(); + if (execOnEnd != null) view.post(execOnEnd); return; } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1e56b5ce8..484eca319 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/layout/channel_item.xml b/app/src/main/res/layout/channel_item.xml index bbdf6779b..6347c4946 100644 --- a/app/src/main/res/layout/channel_item.xml +++ b/app/src/main/res/layout/channel_item.xml @@ -4,11 +4,10 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/itemRoot" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="@dimen/video_item_search_height" android:background="?attr/selectableItemBackground" android:clickable="true" - android:orientation="vertical" - android:padding="12dp"> + android:padding="@dimen/video_item_search_padding"> - + android:layout_marginBottom="@dimen/video_item_search_image_right_margin" + android:ellipsize="end" + android:lines="1" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textSize="@dimen/video_item_search_title_text_size" + tools:text="Channel Title, Lorem ipsum"/> - + - - + + - - diff --git a/app/src/main/res/layout/error_retry.xml b/app/src/main/res/layout/error_retry.xml index edd576e1c..567012f1e 100644 --- a/app/src/main/res/layout/error_retry.xml +++ b/app/src/main/res/layout/error_retry.xml @@ -1,17 +1,17 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index 807a5ac4c..950761b77 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -1,9 +1,10 @@ - + android:layout_height="match_parent" + tools:context=".fragments.channel.ChannelFragment"> + tools:visibility="visible" /> - + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 80aa1ab4c..4608aa8fa 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -1,12 +1,13 @@ - + android:focusableInTouchMode="true" + android:orientation="vertical"> @@ -31,9 +32,9 @@ layout="@layout/error_retry" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerHorizontal="true" + android:layout_gravity="center" android:layout_marginTop="50dp" android:visibility="gone" tools:visibility="visible"/> - + diff --git a/app/src/main/res/layout/fragment_video_detail.xml b/app/src/main/res/layout/fragment_video_detail.xml index aa29b822c..4451d68ab 100644 --- a/app/src/main/res/layout/fragment_video_detail.xml +++ b/app/src/main/res/layout/fragment_video_detail.xml @@ -1,5 +1,5 @@ - - + android:layout_height="wrap_content" + android:orientation="vertical"> - + tools:src="@drawable/dummy_thumbnail" /> + tools:visibility="visible" + android:layout_gravity="center"/> -