NewPipe/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt

463 lines
19 KiB
Kotlin
Raw Normal View History

package org.schabi.newpipe.local.subscription
import android.app.Activity
import android.app.AlertDialog
2020-05-01 20:13:01 +02:00
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.os.Bundle
import android.os.Environment
import android.os.Parcelable
2020-05-01 20:13:01 +02:00
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.Observer
2020-08-27 22:56:12 +02:00
import androidx.lifecycle.ViewModelProvider
import androidx.localbroadcastmanager.content.LocalBroadcastManager
2020-08-27 22:55:57 +02:00
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.nononsenseapps.filepicker.Utils
import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
import com.xwray.groupie.Section
2019-10-10 15:28:57 +02:00
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.State
2020-10-31 21:55:45 +01:00
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
2020-05-01 20:13:01 +02:00
import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.FeedImportExportItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.KEY_FILE_PATH
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
2020-05-01 20:13:01 +02:00
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.report.UserAction
2020-05-01 20:13:01 +02:00
import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper
2020-10-31 21:55:45 +01:00
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.floor
import kotlin.math.max
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: SubscriptionViewModel
private lateinit var subscriptionManager: SubscriptionManager
private val disposables: CompositeDisposable = CompositeDisposable()
private var subscriptionBroadcastReceiver: BroadcastReceiver? = null
2019-10-10 15:28:57 +02:00
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
private val feedGroupsSection = Section()
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
private lateinit var importExportItem: FeedImportExportItem
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
private val subscriptionsSection = Section()
@State
@JvmField
var itemsListState: Parcelable? = null
@State
@JvmField
var feedGroupsListState: Parcelable? = null
@State
@JvmField
var importExportItemExpandedState: Boolean? = null
init {
setHasOptionsMenu(true)
}
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupInitialLayout()
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (activity != null && isVisibleToUser) {
setTitle(activity.getString(R.string.tab_subscriptions))
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
subscriptionManager = SubscriptionManager(requireContext())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_subscription, container, false)
}
override fun onResume() {
super.onResume()
setupBroadcastReceiver()
}
override fun onPause() {
super.onPause()
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
importExportItemExpandedState = importExportItem.isExpanded
if (subscriptionBroadcastReceiver != null && activity != null) {
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
}
}
override fun onDestroy() {
super.onDestroy()
disposables.dispose()
}
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
// Menu
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
val supportActionBar = activity.supportActionBar
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true)
setTitle(getString(R.string.tab_subscriptions))
}
}
private fun setupBroadcastReceiver() {
if (activity == null) return
if (subscriptionBroadcastReceiver != null) {
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
}
val filters = IntentFilter()
filters.addAction(EXPORT_COMPLETE_ACTION)
filters.addAction(IMPORT_COMPLETE_ACTION)
subscriptionBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
_binding?.itemsList?.post {
importExportItem.isExpanded = false
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
}
}
}
LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver!!, filters)
}
private fun onImportFromServiceSelected(serviceId: Int) {
val fragmentManager = fm
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
}
private fun onImportPreviousSelected() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE)
}
private fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
val exportFile = File(Environment.getExternalStorageDirectory(), exportName)
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(requireFragmentManager(), null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_EXPORT_CODE) {
val exportFile = Utils.getFileForUri(data.data!!)
if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) {
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
} else {
2020-10-31 21:55:45 +01:00
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
)
}
} else if (requestCode == REQUEST_IMPORT_CODE) {
val path = Utils.getFileForUri(data.data!!).absolutePath
2020-10-31 21:55:45 +01:00
ImportConfirmationDialog.show(
this,
Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
2020-10-31 21:55:45 +01:00
.putExtra(KEY_VALUE, path)
)
}
}
}
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
// Fragment Views
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
private fun setupInitialLayout() {
Section().apply {
2019-10-10 15:28:57 +02:00
val carouselAdapter = GroupAdapter<GroupieViewHolder>()
carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
carouselAdapter.add(feedGroupsSection)
carouselAdapter.add(FeedGroupAddItem())
carouselAdapter.setOnItemClickListener { item, _ ->
listenerFeedGroups.selected(item)
}
carouselAdapter.setOnItemLongClickListener { item, _ ->
if (item is FeedGroupCardItem) {
if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) {
return@setOnItemLongClickListener false
}
}
listenerFeedGroups.held(item)
return@setOnItemLongClickListener true
}
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
feedGroupsSortMenuItem = HeaderWithMenuItem(
2020-10-31 21:55:45 +01:00
getString(R.string.feed_groups_header_title),
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
menuItemOnClickListener = ::openReorderDialog
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.add(this)
}
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
subscriptionsSection.setHideWhenEmpty(true)
importExportItem = FeedImportExportItem(
2020-10-31 21:55:45 +01:00
{ onImportPreviousSelected() },
{ onImportFromServiceSelected(it) },
{ onExportSelected() },
importExportItemExpandedState ?: false
)
groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
}
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
val shouldUseGridLayout = shouldUseGridLayout()
groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
binding.itemsList.adapter = groupAdapter
2020-08-27 22:56:12 +02:00
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleResult) })
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleFeedGroups) })
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
val commands = arrayOf(
2020-10-31 21:55:45 +01:00
getString(R.string.share),
getString(R.string.unsubscribe)
)
val actions = DialogInterface.OnClickListener { _, i ->
when (i) {
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url)
1 -> deleteChannel(selectedItem)
}
}
val dialogTitleBinding = DialogTitleBinding.inflate(LayoutInflater.from(requireContext()))
dialogTitleBinding.root.isSelected = true
dialogTitleBinding.itemTitleView.text = selectedItem.name
dialogTitleBinding.itemAdditionalDetails.visibility = View.GONE
AlertDialog.Builder(requireContext())
.setCustomTitle(dialogTitleBinding.root)
.setItems(commands, actions)
.create()
.show()
}
private fun deleteChannel(selectedItem: ChannelInfoItem) {
2020-10-31 21:55:45 +01:00
disposables.add(
subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
}
)
}
override fun doInitialLoadLogic() = Unit
override fun startLoading(forceLoad: Boolean) = Unit
private val listenerFeedGroups = object : OnClickGesture<Item<*>>() {
override fun selected(selectedItem: Item<*>?) {
when (selectedItem) {
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
is FeedGroupAddItem -> FeedGroupDialog.newInstance().show(fm, null)
}
}
override fun held(selectedItem: Item<*>?) {
when (selectedItem) {
is FeedGroupCardItem -> FeedGroupDialog.newInstance(selectedItem.groupId).show(fm, null)
}
}
}
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() {
2020-10-31 21:55:45 +01:00
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name
)
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
}
override fun handleResult(result: SubscriptionState) {
super.handleResult(result)
val shouldUseGridLayout = shouldUseGridLayout()
when (result) {
is SubscriptionState.LoadedState -> {
result.subscriptions.forEach {
if (it is ChannelItem) {
it.gesturesListener = listenerChannelItem
it.itemVersion = when {
shouldUseGridLayout -> ChannelItem.ItemVersion.GRID
else -> ChannelItem.ItemVersion.MINI
}
}
}
subscriptionsSection.update(result.subscriptions)
subscriptionsSection.setHideWhenEmpty(false)
if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
binding.itemsList.post {
importExportItem.isExpanded = true
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
}
}
if (itemsListState != null) {
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
itemsListState = null
}
}
is SubscriptionState.ErrorState -> {
result.error?.let { onError(result.error) }
}
}
}
private fun handleFeedGroups(groups: List<Group>) {
feedGroupsSection.update(groups)
if (feedGroupsListState != null) {
feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState)
feedGroupsListState = null
}
feedGroupsSortMenuItem.showMenuItem = groups.size > 1
binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
}
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
// Contract
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
override fun showLoading() {
super.showLoading()
binding.itemsList.animate(false, 100)
}
override fun hideLoading() {
super.hideLoading()
binding.itemsList.animate(true, 200)
}
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
override fun onError(exception: Throwable): Boolean {
if (super.onError(exception)) return true
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error)
return true
}
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
// Grid Mode
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
// TODO: Move these out of this class, as it can be reused
private fun shouldUseGridLayout(): Boolean {
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
2020-10-31 21:55:45 +01:00
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
return when (listMode) {
getString(R.string.list_view_mode_auto_key) -> {
val configuration = resources.configuration
2020-10-31 21:55:45 +01:00
(
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
)
}
getString(R.string.list_view_mode_grid_key) -> true
else -> false
}
}
private fun getGridSpanCount(): Int {
val minWidth = resources.getDimensionPixelSize(R.dimen.channel_item_grid_min_width)
return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt())
}
companion object {
private const val REQUEST_EXPORT_CODE = 666
private const val REQUEST_IMPORT_CODE = 667
}
}