From 45aa445b5032715f4432d6f670c3c54d742e9e65 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sun, 1 Sep 2024 18:30:18 +0530 Subject: [PATCH] Fix crash when minimizing the channel about tab --- .../detail/BaseDescriptionFragment.java | 281 ------------------ .../list/channel/AboutChannelFragment.kt | 7 +- .../java/org/schabi/newpipe/ktx/Bundle.kt | 4 + .../components/channel/AboutChannelSection.kt | 38 ++- .../channel/ParcelableChannelInfo.kt | 21 ++ .../components/metadata/ImageMetadataItem.kt | 2 - 6 files changed, 53 insertions(+), 300 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java create mode 100644 app/src/main/java/org/schabi/newpipe/ui/components/channel/ParcelableChannelInfo.kt diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java deleted file mode 100644 index 4789b02e65b..00000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/BaseDescriptionFragment.java +++ /dev/null @@ -1,281 +0,0 @@ -package org.schabi.newpipe.fragments.detail; - -import static android.text.TextUtils.isEmpty; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; -import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD; - -import android.graphics.Typeface; -import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.method.LinkMovementMethod; -import android.text.style.ClickableSpan; -import android.text.style.StyleSpan; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.widget.TooltipCompat; -import androidx.core.text.HtmlCompat; - -import com.google.android.material.chip.Chip; - -import org.schabi.newpipe.BaseFragment; -import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.FragmentDescriptionBinding; -import org.schabi.newpipe.databinding.ItemMetadataBinding; -import org.schabi.newpipe.databinding.ItemMetadataTagsBinding; -import org.schabi.newpipe.extractor.Image; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.stream.Description; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.external_communication.ShareUtils; -import org.schabi.newpipe.util.image.ImageStrategy; -import org.schabi.newpipe.util.text.TextLinkifier; - -import java.util.List; - -import io.reactivex.rxjava3.disposables.CompositeDisposable; - -public abstract class BaseDescriptionFragment extends BaseFragment { - private final CompositeDisposable descriptionDisposables = new CompositeDisposable(); - protected FragmentDescriptionBinding binding; - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - binding = FragmentDescriptionBinding.inflate(inflater, container, false); - setupDescription(); - setupMetadata(inflater, binding.detailMetadataLayout); - addTagsMetadataItem(inflater, binding.detailMetadataLayout); - return binding.getRoot(); - } - - @Override - public void onDestroy() { - descriptionDisposables.clear(); - super.onDestroy(); - } - - /** - * Get the description to display. - * @return description object, if available - */ - @Nullable - protected abstract Description getDescription(); - - /** - * Get the streaming service. Used for generating description links. - * @return streaming service - */ - @NonNull - protected abstract StreamingService getService(); - - /** - * Get the streaming service ID. Used for tag links. - * @return service ID - */ - protected abstract int getServiceId(); - - /** - * Get the URL of the described video or audio, used to generate description links. - * @return stream URL - */ - @Nullable - protected abstract String getStreamUrl(); - - /** - * Get the list of tags to display below the description. - * @return tag list - */ - @NonNull - public abstract List getTags(); - - /** - * Add additional metadata to display. - * @param inflater LayoutInflater - * @param layout detailMetadataLayout - */ - protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout); - - private void setupDescription() { - final Description description = getDescription(); - if (description == null || isEmpty(description.getContent()) - || description == Description.EMPTY_DESCRIPTION) { - binding.detailDescriptionView.setVisibility(View.GONE); - binding.detailSelectDescriptionButton.setVisibility(View.GONE); - return; - } - - // start with disabled state. This also loads description content (!) - disableDescriptionSelection(); - - binding.detailSelectDescriptionButton.setOnClickListener(v -> { - if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) { - disableDescriptionSelection(); - } else { - // enable selection only when button is clicked to prevent flickering - enableDescriptionSelection(); - } - }); - } - - private void enableDescriptionSelection() { - binding.detailDescriptionNoteView.setVisibility(View.VISIBLE); - binding.detailDescriptionView.setTextIsSelectable(true); - - final String buttonLabel = getString(R.string.description_select_disable); - binding.detailSelectDescriptionButton.setContentDescription(buttonLabel); - TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel); - binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close); - } - - private void disableDescriptionSelection() { - // show description content again, otherwise some links are not clickable - final Description description = getDescription(); - if (description != null) { - TextLinkifier.fromDescription(binding.detailDescriptionView, - description, HtmlCompat.FROM_HTML_MODE_LEGACY, - getService(), getStreamUrl(), - descriptionDisposables, SET_LINK_MOVEMENT_METHOD); - } - - binding.detailDescriptionNoteView.setVisibility(View.GONE); - binding.detailDescriptionView.setTextIsSelectable(false); - - final String buttonLabel = getString(R.string.description_select_enable); - binding.detailSelectDescriptionButton.setContentDescription(buttonLabel); - TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel); - binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all); - } - - protected void addMetadataItem(final LayoutInflater inflater, - final LinearLayout layout, - final boolean linkifyContent, - @StringRes final int type, - @NonNull final String content) { - if (isBlank(content)) { - return; - } - - final ItemMetadataBinding itemBinding = - ItemMetadataBinding.inflate(inflater, layout, false); - - itemBinding.metadataTypeView.setText(type); - itemBinding.metadataTypeView.setOnLongClickListener(v -> { - ShareUtils.copyToClipboard(requireContext(), content); - return true; - }); - - if (linkifyContent) { - TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null, - descriptionDisposables, SET_LINK_MOVEMENT_METHOD); - } else { - itemBinding.metadataContentView.setText(content); - } - - itemBinding.metadataContentView.setClickable(true); - - layout.addView(itemBinding.getRoot()); - } - - private String imageSizeToText(final int heightOrWidth) { - if (heightOrWidth < 0) { - return getString(R.string.question_mark); - } else { - return String.valueOf(heightOrWidth); - } - } - - protected void addImagesMetadataItem(final LayoutInflater inflater, - final LinearLayout layout, - @StringRes final int type, - final List images) { - final String preferredImageUrl = ImageStrategy.choosePreferredImage(images); - if (preferredImageUrl == null) { - return; // null will be returned in case there is no image - } - - final ItemMetadataBinding itemBinding = - ItemMetadataBinding.inflate(inflater, layout, false); - itemBinding.metadataTypeView.setText(type); - - final SpannableStringBuilder urls = new SpannableStringBuilder(); - for (final Image image : images) { - if (urls.length() != 0) { - urls.append(", "); - } - final int entryBegin = urls.length(); - - if (image.getHeight() != Image.HEIGHT_UNKNOWN - || image.getWidth() != Image.WIDTH_UNKNOWN - // if even the resolution level is unknown, ?x? will be shown - || image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) { - urls.append(imageSizeToText(image.getHeight())); - urls.append('x'); - urls.append(imageSizeToText(image.getWidth())); - } else { - switch (image.getEstimatedResolutionLevel()) { - case LOW -> urls.append(getString(R.string.image_quality_low)); - case MEDIUM -> urls.append(getString(R.string.image_quality_medium)); - case HIGH -> urls.append(getString(R.string.image_quality_high)); - default -> { - // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out - } - } - } - - urls.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull final View widget) { - ShareUtils.openUrlInBrowser(requireContext(), image.getUrl()); - } - }, entryBegin, urls.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - if (preferredImageUrl.equals(image.getUrl())) { - urls.setSpan(new StyleSpan(Typeface.BOLD), entryBegin, urls.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - itemBinding.metadataContentView.setText(urls); - itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance()); - layout.addView(itemBinding.getRoot()); - } - - private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { - final List tags = getTags(); - - if (!tags.isEmpty()) { - final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false); - - tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> { - final Chip chip = (Chip) inflater.inflate(R.layout.chip, - itemBinding.metadataTagsChips, false); - chip.setText(tag); - chip.setOnClickListener(this::onTagClick); - chip.setOnLongClickListener(this::onTagLongClick); - itemBinding.metadataTagsChips.addView(chip); - }); - - layout.addView(itemBinding.getRoot()); - } - } - - private void onTagClick(final View chip) { - if (getParentFragment() != null) { - NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(), - getServiceId(), ((Chip) chip).getText().toString()); - } - } - - private boolean onTagLongClick(final View chip) { - ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString()); - return true; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/AboutChannelFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/AboutChannelFragment.kt index a4c8ae3f5ce..510a940be37 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/AboutChannelFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/AboutChannelFragment.kt @@ -9,8 +9,9 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.compose.content import org.schabi.newpipe.extractor.channel.ChannelInfo -import org.schabi.newpipe.ktx.serializable +import org.schabi.newpipe.ktx.parcelable import org.schabi.newpipe.ui.components.channel.AboutChannelSection +import org.schabi.newpipe.ui.components.channel.ParcelableChannelInfo import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.KEY_INFO @@ -22,7 +23,7 @@ class AboutChannelFragment : Fragment() { ) = content { AppTheme { Surface(color = MaterialTheme.colorScheme.background) { - AboutChannelSection(requireArguments().serializable(KEY_INFO)!!) + AboutChannelSection(requireArguments().parcelable(KEY_INFO)!!) } } } @@ -30,7 +31,7 @@ class AboutChannelFragment : Fragment() { companion object { @JvmStatic fun getInstance(channelInfo: ChannelInfo) = AboutChannelFragment().apply { - arguments = bundleOf(KEY_INFO to channelInfo) + arguments = bundleOf(KEY_INFO to ParcelableChannelInfo(channelInfo)) } } } diff --git a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt b/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt index e248b8b6c63..22fd87142c5 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/Bundle.kt @@ -5,6 +5,10 @@ import android.os.Parcelable import androidx.core.os.BundleCompat import java.io.Serializable +inline fun Bundle.parcelable(key: String?): T? { + return BundleCompat.getParcelable(this, key, T::class.java) +} + inline fun Bundle.parcelableArrayList(key: String?): ArrayList? { return BundleCompat.getParcelableArrayList(this, key, T::class.java) } diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/channel/AboutChannelSection.kt b/app/src/main/java/org/schabi/newpipe/ui/components/channel/AboutChannelSection.kt index 15c7d44469f..a37d7711eb3 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/channel/AboutChannelSection.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/channel/AboutChannelSection.kt @@ -13,7 +13,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.Image.ResolutionLevel import org.schabi.newpipe.ui.components.metadata.ImageMetadataItem import org.schabi.newpipe.ui.components.metadata.MetadataItem import org.schabi.newpipe.ui.components.metadata.TagsSection @@ -22,18 +23,18 @@ import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NO_SERVICE_ID @Composable -fun AboutChannelSection(channelInfo: ChannelInfo) { +fun AboutChannelSection(channelInfo: ParcelableChannelInfo) { // This tab currently holds little information, so a lazy column isn't needed here. Column( modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { - val description = channelInfo.description - if (!description.isNullOrEmpty()) { + val (serviceId, description, count, avatars, banners, tags) = channelInfo + + if (description.isNotEmpty()) { Text(text = description) } - val count = channelInfo.subscriberCount if (count != -1L) { MetadataItem( title = R.string.metadata_subscribers, @@ -41,16 +42,16 @@ fun AboutChannelSection(channelInfo: ChannelInfo) { ) } - if (channelInfo.avatars.isNotEmpty()) { - ImageMetadataItem(R.string.metadata_avatars, channelInfo.avatars) + if (avatars.isNotEmpty()) { + ImageMetadataItem(R.string.metadata_avatars, avatars) } - if (channelInfo.banners.isNotEmpty()) { - ImageMetadataItem(R.string.metadata_banners, channelInfo.banners) + if (banners.isNotEmpty()) { + ImageMetadataItem(R.string.metadata_banners, banners) } - if (channelInfo.tags.isNotEmpty()) { - TagsSection(channelInfo.serviceId, channelInfo.tags) + if (tags.isNotEmpty()) { + TagsSection(serviceId, tags) } } } @@ -59,9 +60,18 @@ fun AboutChannelSection(channelInfo: ChannelInfo) { @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun AboutChannelSectionPreview() { - val info = ChannelInfo(NO_SERVICE_ID, "", "", "", "") - info.description = "This is an example description" - info.subscriberCount = 10 + val images = listOf( + Image("https://example.com/image_low.png", 16, 16, ResolutionLevel.LOW), + Image("https://example.com/image_mid.png", 32, 32, ResolutionLevel.MEDIUM) + ) + val info = ParcelableChannelInfo( + serviceId = NO_SERVICE_ID, + description = "This is an example description", + subscriberCount = 10, + avatars = images, + banners = images, + tags = listOf("Tag 1", "Tag 2") + ) AppTheme { Surface(color = MaterialTheme.colorScheme.background) { diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/channel/ParcelableChannelInfo.kt b/app/src/main/java/org/schabi/newpipe/ui/components/channel/ParcelableChannelInfo.kt new file mode 100644 index 00000000000..f8f118c0502 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/components/channel/ParcelableChannelInfo.kt @@ -0,0 +1,21 @@ +package org.schabi.newpipe.ui.components.channel + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.channel.ChannelInfo + +@Parcelize +data class ParcelableChannelInfo( + val serviceId: Int, + val description: String, + val subscriberCount: Long, + val avatars: List, + val banners: List, + val tags: List +) : Parcelable { + constructor(channelInfo: ChannelInfo) : this( + channelInfo.serviceId, channelInfo.description, channelInfo.subscriberCount, + channelInfo.avatars, channelInfo.banners, channelInfo.tags + ) +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt index 66cd5c5bd7f..8d282c2345e 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/metadata/ImageMetadataItem.kt @@ -23,7 +23,6 @@ import org.schabi.newpipe.extractor.Image import org.schabi.newpipe.extractor.Image.ResolutionLevel import org.schabi.newpipe.ui.theme.AppTheme import org.schabi.newpipe.util.image.ImageStrategy -import org.schabi.newpipe.util.image.PreferredImageQuality @Composable fun ImageMetadataItem(@StringRes title: Int, images: List) { @@ -74,7 +73,6 @@ private fun convertImagesToLinks(context: Context, images: List): Annotat @Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ImageMetadataItemPreview() { - ImageStrategy.setPreferredImageQuality(PreferredImageQuality.MEDIUM) val images = listOf( Image("https://example.com/image_low.png", 16, 16, ResolutionLevel.LOW), Image("https://example.com/image_mid.png", 32, 32, ResolutionLevel.MEDIUM)