package org.schabi.newpipe.fragments.list.channel; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; import android.text.TextUtils; 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.TextView; import com.jakewharton.rxbinding2.view.RxView; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; import org.schabi.newpipe.fragments.subscription.SubscriptionService; import org.schabi.newpipe.report.UserAction; import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.KioskTranslator; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import java.util.List; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Action; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor; import static org.schabi.newpipe.util.AnimationUtils.animateTextColor; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class ChannelFragment extends BaseListInfoFragment { private CompositeDisposable disposables = new CompositeDisposable(); private Disposable subscribeButtonMonitor; private SubscriptionService subscriptionService; /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ private View headerRootLayout; private ImageView headerChannelBanner; private ImageView headerAvatarView; private TextView headerTitleView; private TextView headerSubscribersTextView; private Button headerSubscribeButton; private MenuItem menuRssButton; public static ChannelFragment getInstance(int serviceId, String url, String name) { ChannelFragment instance = new ChannelFragment(); instance.setInitialData(serviceId, url, name); return instance; } /*////////////////////////////////////////////////////////////////////////// // LifeCycle //////////////////////////////////////////////////////////////////////////*/ @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if(activity != null && useAsFrontPage && isVisibleToUser) { try { activity.getSupportActionBar().setTitle(currentInfo.name); } catch (Exception e) { onError(e); } } } @Override public void onAttach(Context context) { super.onAttach(context); subscriptionService = SubscriptionService.getInstance(); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_channel, container, false); return v; } @Override public void onDestroy() { super.onDestroy(); if (disposables != null) disposables.clear(); if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); } /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ protected View getListHeader() { headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false); headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image); headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view); headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view); headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view); headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button); return headerRootLayout; } /*////////////////////////////////////////////////////////////////////////// // Menu //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); ActionBar supportActionBar = activity.getSupportActionBar(); if(useAsFrontPage) { supportActionBar.setDisplayHomeAsUpEnabled(false); } else { inflater.inflate(R.menu.menu_channel, menu); if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); menuRssButton = menu.findItem(R.id.menu_item_rss); if (currentInfo != null) { menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.feed_url)); } } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_rss: { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(currentInfo.feed_url)); startActivity(intent); return true; } case R.id.menu_item_openInBrowser: { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(currentInfo.url)); startActivity(intent); return true; } case R.id.menu_item_share: { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, currentInfo.url); startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title))); return true; } default: return super.onOptionsItemSelected(item); } } /*////////////////////////////////////////////////////////////////////////// // Channel Subscription //////////////////////////////////////////////////////////////////////////*/ private static final int BUTTON_DEBOUNCE_INTERVAL = 100; private void monitorSubscription(final ChannelInfo info) { final Consumer onError = new Consumer() { @Override public void accept(Throwable throwable) throws Exception { animateView(headerSubscribeButton, false, 100); showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Get subscription status", 0); } }; final Observable> observable = subscriptionService.subscriptionTable() .getSubscription(info.service_id, info.url) .toObservable(); disposables.add(observable .observeOn(AndroidSchedulers.mainThread()) .subscribe(getSubscribeUpdateMonitor(info), onError)); disposables.add(observable // Some updates are very rapid (when calling the updateSubscription(info), for example) // so only update the UI for the latest emission ("sync" the subscribe button's state) .debounce(100, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer>() { @Override public void accept(List subscriptionEntities) throws Exception { updateSubscribeButton(!subscriptionEntities.isEmpty()); } }, onError)); } private Function mapOnSubscribe(final SubscriptionEntity subscription) { return new Function() { @Override public Object apply(@NonNull Object o) throws Exception { subscriptionService.subscriptionTable().insert(subscription); return o; } }; } private Function mapOnUnsubscribe(final SubscriptionEntity subscription) { return new Function() { @Override public Object apply(@NonNull Object o) throws Exception { subscriptionService.subscriptionTable().delete(subscription); return o; } }; } private void updateSubscription(final ChannelInfo info) { if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]"); final Action onComplete = new Action() { @Override public void run() throws Exception { if (DEBUG) Log.d(TAG, "Updated subscription: " + info.url); } }; final Consumer onError = new Consumer() { @Override public void accept(@NonNull Throwable throwable) throws Exception { onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.service_id), "Updating Subscription for " + info.url, R.string.subscription_update_failed); } }; disposables.add(subscriptionService.updateChannelInfo(info) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(onComplete, onError)); } private Disposable monitorSubscribeButton(final Button subscribeButton, final Function action) { final Consumer onNext = new Consumer() { @Override public void accept(@NonNull Object o) throws Exception { if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!"); } }; final Consumer onError = new Consumer() { @Override public void accept(@NonNull Throwable throwable) throws Exception { onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Subscription Change", R.string.subscription_change_failed); } }; /* Emit clicks from main thread unto io thread */ return RxView.clicks(subscribeButton) .subscribeOn(AndroidSchedulers.mainThread()) .observeOn(Schedulers.io()) .debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks .map(action) .subscribe(onNext, onError); } private Consumer> getSubscribeUpdateMonitor(final ChannelInfo info) { return new Consumer>() { @Override public void accept(List subscriptionEntities) throws Exception { if (DEBUG) Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]"); if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); if (subscriptionEntities.isEmpty()) { if (DEBUG) Log.d(TAG, "No subscription to this channel!"); SubscriptionEntity channel = new SubscriptionEntity(); channel.setServiceId(info.service_id); channel.setUrl(info.url); channel.setData(info.name, info.avatar_url, info.description, info.subscriber_count); subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel)); } else { if (DEBUG) Log.d(TAG, "Found subscription to this channel!"); final SubscriptionEntity subscription = subscriptionEntities.get(0); subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription)); } } }; } private void updateSubscribeButton(boolean isSubscribed) { if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]"); boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE; int backgroundDuration = isButtonVisible ? 300 : 0; int textDuration = isButtonVisible ? 200 : 0; int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color); int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color); int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color); int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color); if (!isSubscribed) { headerSubscribeButton.setText(R.string.subscribe_button_title); animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground); animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText); } else { headerSubscribeButton.setText(R.string.subscribed_button_title); animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground); animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText); } animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100); } /*////////////////////////////////////////////////////////////////////////// // Load and handle //////////////////////////////////////////////////////////////////////////*/ @Override protected Single loadMoreItemsLogic() { return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextItemsUrl); } @Override protected Single loadResult(boolean forceLoad) { return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad); } /*////////////////////////////////////////////////////////////////////////// // Contract //////////////////////////////////////////////////////////////////////////*/ @Override public void showLoading() { super.showLoading(); imageLoader.cancelDisplayTask(headerChannelBanner); imageLoader.cancelDisplayTask(headerAvatarView); animateView(headerSubscribeButton, false, 100); } @Override public void handleResult(@NonNull ChannelInfo result) { super.handleResult(result); headerRootLayout.setVisibility(View.VISIBLE); imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS); imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS); if (result.subscriber_count != -1) { headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.subscriber_count)); headerSubscribersTextView.setVisibility(View.VISIBLE); } else headerSubscribersTextView.setVisibility(View.GONE); if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.feed_url)); if (!result.errors.isEmpty()) { showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.service_id), result.url, 0); } if (disposables != null) disposables.clear(); if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose(); updateSubscription(result); monitorSubscription(result); } @Override public void handleNextItems(ListExtractor.NextItemsResult result) { super.handleNextItems(result); if (!result.errors.isEmpty()) { showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), "Get next page of: " + url, R.string.general_error); } } /*////////////////////////////////////////////////////////////////////////// // OnError //////////////////////////////////////////////////////////////////////////*/ @Override protected boolean onError(Throwable exception) { if (super.onError(exception)) return true; int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId); return true; } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ @Override public void setTitle(String title) { super.setTitle(title); headerTitleView.setText(title); } }