package com.thebluealliance.androidclient.fragments;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.firebase.client.Firebase;
import com.thebluealliance.androidclient.R;
import com.thebluealliance.androidclient.TbaLogger;
import com.thebluealliance.androidclient.ViewUtilities;
import com.thebluealliance.androidclient.adapters.AnimatedRecyclerMultiAdapter;
import com.thebluealliance.androidclient.adapters.ListViewAdapter;
import com.thebluealliance.androidclient.database.DatabaseWriter;
import com.thebluealliance.androidclient.datafeed.retrofit.FirebaseAPI;
import com.thebluealliance.androidclient.di.components.FragmentComponent;
import com.thebluealliance.androidclient.di.components.HasFragmentComponent;
import com.thebluealliance.androidclient.firebase.FirebaseChildType;
import com.thebluealliance.androidclient.firebase.ResumeableRxFirebase;
import com.thebluealliance.androidclient.gcm.notifications.BaseNotification;
import com.thebluealliance.androidclient.gcm.notifications.NotificationTypes;
import com.thebluealliance.androidclient.itemviews.AllianceSelectionNotificationItemView;
import com.thebluealliance.androidclient.itemviews.AwardsPostedNotificationItemView;
import com.thebluealliance.androidclient.itemviews.CompLevelStartingNotificationItemView;
import com.thebluealliance.androidclient.itemviews.GenericNotificationItemView;
import com.thebluealliance.androidclient.itemviews.ScheduleUpdatedNotificationItemView;
import com.thebluealliance.androidclient.itemviews.ScoreNotificationItemView;
import com.thebluealliance.androidclient.itemviews.UpcomingMatchNotificationItemView;
import com.thebluealliance.androidclient.listitems.ListItem;
import com.thebluealliance.androidclient.listitems.gameday.GamedayTickerFilterCheckbox;
import com.thebluealliance.androidclient.models.FirebaseNotification;
import com.thebluealliance.androidclient.viewmodels.AllianceSelectionNotificationViewModel;
import com.thebluealliance.androidclient.viewmodels.AwardsPostedNotificationViewModel;
import com.thebluealliance.androidclient.viewmodels.CompLevelStartingNotificationViewModel;
import com.thebluealliance.androidclient.viewmodels.GenericNotificationViewModel;
import com.thebluealliance.androidclient.viewmodels.ScheduleUpdatedNotificationViewModel;
import com.thebluealliance.androidclient.viewmodels.ScoreNotificationViewModel;
import com.thebluealliance.androidclient.viewmodels.UpcomingMatchNotificationViewModel;
import com.thebluealliance.androidclient.views.NoDataView;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.Bind;
import butterknife.ButterKnife;
import io.nlopez.smartadapters.utils.Mapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
public abstract class FirebaseTickerFragment extends Fragment implements Action1<List<FirebaseNotification>>, View.OnClickListener {
public static final String FIREBASE_URL_DEFAULT = "https://thebluealliance.firebaseio.com/";
public static final int FIREBASE_LOAD_DEPTH_DEFAULT = 25;
private static final int ANIMATION_DURATION = 300;
private static final float DIMMED_ALPHA = 0.6f;
private enum FirebaseChildNodesState {
LOADING,
HAS_CHILDREN,
NO_CHILDREN
}
@Inject DatabaseWriter mWriter;
@Inject @Named("firebase_api") FirebaseAPI mFirebaseApi;
@Bind(R.id.list) RecyclerView mNotificationsRecyclerView;
@Bind(R.id.filter_list) ListView mFilterListView;
@Bind(R.id.filter_list_container) View mFilterListContainer;
@Bind(R.id.no_data) NoDataView mNoDataView;
@Bind(R.id.progress) ProgressBar mProgressBar;
@Bind(R.id.left_button) TextView mLeftButton;
@Bind(R.id.right_button) TextView mRightButton;
@Bind(R.id.filter_shadow) View mShadow;
@Bind(R.id.foreground_dim) View mForegroundDim;
private AnimatedRecyclerMultiAdapter mNotificationsAdapter;
private ListViewAdapter mNotificationFilterAdapter;
private List<BaseNotification> mAllNotifications = new ArrayList<>();
private boolean mAreFilteredNotificationsVisible = false;
private ResumeableRxFirebase mFirebaseSubscriber;
private LinearLayoutManager mLayoutManager;
private Parcelable mListState;
private Set<String> enabledNotifications;
private boolean filterListShowing;
private FirebaseChildNodesState mChildNodeState = FirebaseChildNodesState.LOADING;
private String mFirebaseUrl;
private int mFirebaseLoadDepth;
protected FragmentComponent mComponent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getActivity() instanceof HasFragmentComponent) {
mComponent = ((HasFragmentComponent) getActivity()).getComponent();
}
inject();
loadFirebaseParams();
mFirebaseSubscriber = new ResumeableRxFirebase();
// Delivery will be resumed once the view hierarchy is created
mFirebaseSubscriber.pauseDelivery();
mFirebaseSubscriber.getObservable()
.onBackpressureBuffer()
.filter(childEvent -> childEvent != null && childEvent.eventType == FirebaseChildType.CHILD_ADDED)
.map(childEvent1 -> childEvent1.snapshot.getValue(FirebaseNotification.class))
.buffer(5, TimeUnit.SECONDS, 5)
.filter(itemList -> itemList != null && !itemList.isEmpty())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this, throwable -> {
TbaLogger.e("Firebase error: " + throwable);
throwable.printStackTrace();
// Show the "none found" warning
mProgressBar.setVisibility(View.GONE);
mNotificationsRecyclerView.setVisibility(View.GONE);
mNoDataView.setVisibility(View.VISIBLE);
mNoDataView.setText(R.string.firebase_no_matching_items);
});
Firebase.setAndroidContext(getActivity());
Firebase ticker = new Firebase(mFirebaseUrl);
ticker.orderByKey().limitToLast(mFirebaseLoadDepth).addChildEventListener(mFirebaseSubscriber);
Observable<JsonElement> oneItem = mFirebaseApi.getOneItemFromNode(getFirebaseUrlSuffix());
if (oneItem != null) {
oneItem.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result == null || result.isJsonNull()) {
mChildNodeState = FirebaseChildNodesState.NO_CHILDREN;
} else if (result.isJsonObject()) {
if (((JsonObject) result).entrySet().size() > 0) {
mChildNodeState = FirebaseChildNodesState.HAS_CHILDREN;
} else {
mChildNodeState = FirebaseChildNodesState.NO_CHILDREN;
}
}
updateViewVisibility();
}, throwable -> {
TbaLogger.e("Firebase rest error: " + throwable);
throwable.printStackTrace();
// net error getting item count, show no data view
mChildNodeState = FirebaseChildNodesState.NO_CHILDREN;
updateViewVisibility();
});
}
filterListShowing = false;
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_firebase_ticker, null);
ButterKnife.bind(this, v);
mNoDataView.setImage(R.drawable.ic_notifications_black_48dp);
if (mNotificationFilterAdapter == null) {
mNotificationFilterAdapter = createFilterListAdapter();
}
mFilterListView.setAdapter(mNotificationFilterAdapter);
mLeftButton.setOnClickListener(this);
mRightButton.setOnClickListener(this);
mNotificationsRecyclerView.setHasFixedSize(true);
mNotificationsRecyclerView.setItemAnimator(new DefaultItemAnimator());
mLayoutManager = new LinearLayoutManager(getContext());
mNotificationsRecyclerView.setLayoutManager(mLayoutManager);
if (mNotificationsAdapter != null) {
mNotificationsRecyclerView.setAdapter(mNotificationsAdapter);
mLayoutManager.onRestoreInstanceState(mListState);
TbaLogger.d("onCreateView: using existing adapter");
} else {
mNotificationsAdapter = new AnimatedRecyclerMultiAdapter(createAdapterMapper(), new ArrayList<>());
mNotificationsRecyclerView.setAdapter(mNotificationsAdapter);
}
updateViewVisibility();
// Do this after layout so that the filter container will have a defined height
ViewUtilities.runOnceAfterLayout(mFilterListContainer, () -> hideFilter(false, null));
return v;
}
@Override public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
hideFilter(false, null);
}
private void updateViewVisibility() {
if (mAllNotifications.size() > 0) {
if (mAreFilteredNotificationsVisible) {
// We've received at least one notification from the client library, and it's
// visible with the current applied filter. Show the list!
ViewCrossfader.create(ANIMATION_DURATION)
.fadeIn(mNotificationsRecyclerView)
.fadeOut(mProgressBar)
.fadeOut(mNoDataView)
.start();
} else {
// We've received at least one notification from the client library, but no
// notification are visible with the current applied filter. Show the no data view,
// but with a special "none found with that filter" message
mNoDataView.setText(R.string.firebase_no_matching_items);
ViewCrossfader.create(ANIMATION_DURATION)
.fadeOut(mNotificationsRecyclerView)
.fadeOut(mProgressBar)
.fadeIn(mNoDataView)
.start();
}
} else if (mChildNodeState == FirebaseChildNodesState.NO_CHILDREN) {
// We've received the result of the REST call to the server and the list is (at least
// for now) definitely empty. Show the no data view
mNoDataView.setText(R.string.firebase_empty_ticker);
ViewCrossfader.create(ANIMATION_DURATION)
.fadeOut(mNotificationsRecyclerView)
.fadeOut(mProgressBar)
.fadeIn(mNoDataView)
.start();
} else {
// We haven't yet received any notifications from the client library, but we also
// aren't certain that the list is empty. Show the spinner.
ViewCrossfader.create(ANIMATION_DURATION)
.fadeOut(mNotificationsRecyclerView)
.fadeIn(mProgressBar)
.fadeOut(mNoDataView)
.start();
}
}
@Override
public void onResume() {
super.onResume();
mFirebaseSubscriber.resumeDelivery();
}
@Override
public void onPause() {
super.onPause();
if (mNotificationsRecyclerView != null) {
TbaLogger.d("onPause: saving adapter");
mNotificationsAdapter = (AnimatedRecyclerMultiAdapter) mNotificationsRecyclerView.getAdapter();
mListState = mLayoutManager.onSaveInstanceState();
}
mFirebaseSubscriber.pauseDelivery();
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.right_button) {
if (!filterListShowing) {
showFilter(true);
enabledNotifications = getEnabledNotificationKeys();
} else {
hideFilter(true, this::updateList);
}
} else if (id == R.id.left_button) {
// Don't reset the adapter until the list is closed
// This prevents the checkboxes from briefly flashing to their default state before
// the list is hidden
hideFilter(true, () -> {
mNotificationFilterAdapter = createFilterListAdapter(enabledNotifications);
mFilterListView.setAdapter(mNotificationFilterAdapter);
});
}
}
private void showFilter(boolean animate) {
filterListShowing = true;
mFilterListContainer.setVisibility(View.VISIBLE);
mLeftButton.setVisibility(View.VISIBLE);
mLeftButton.setText(R.string.firebase_cancel);
mRightButton.setText(R.string.firebase_apply_filter);
mShadow.setVisibility(View.GONE);
if (animate) {
mFilterListView.animate()
.alpha(1.0f)
.setDuration(ANIMATION_DURATION)
.setStartDelay(ANIMATION_DURATION)
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
mFilterListView.setAlpha(1.0f);
}
}).start();
mFilterListContainer.animate()
.translationY(0)
.setDuration(ANIMATION_DURATION)
.setStartDelay(0)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
mFilterListContainer.setTranslationY(0);
}
}).start();
mForegroundDim.animate()
.alpha(DIMMED_ALPHA)
.setDuration(ANIMATION_DURATION)
.setStartDelay(0)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
mForegroundDim.setAlpha(DIMMED_ALPHA);
}
}).start();
} else {
mFilterListContainer.setTranslationY(0);
mForegroundDim.setAlpha(DIMMED_ALPHA);
mFilterListView.setAlpha(1.0f);
}
}
private void hideFilter(boolean animate, Runnable onHidden) {
filterListShowing = false;
mLeftButton.setVisibility(View.GONE);
mRightButton.setText(R.string.firebase_filter);
int viewHeight = getView().getHeight();
if (animate) {
mFilterListView.animate()
.alpha(0.0f)
.setDuration(ANIMATION_DURATION)
.setStartDelay(0)
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
mFilterListView.setAlpha(0.0f);
}
}).start();
mFilterListContainer.animate()
.translationY(viewHeight)
.setDuration(ANIMATION_DURATION)
.setStartDelay(ANIMATION_DURATION)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
mFilterListContainer.setTranslationY(viewHeight);
mFilterListContainer.setVisibility(View.GONE);
mShadow.setVisibility(View.VISIBLE);
if (onHidden != null) {
onHidden.run();
}
}
}).start();
mForegroundDim.animate()
.alpha(0.0f)
.setDuration(ANIMATION_DURATION)
.setStartDelay(ANIMATION_DURATION)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
mForegroundDim.setAlpha(0.0f);
}
}).start();
} else {
mFilterListContainer.setTranslationY(mFilterListContainer.getHeight());
mFilterListView.setAlpha(0.0f);
mShadow.setVisibility(View.VISIBLE);
mForegroundDim.setAlpha(0.0f);
if (onHidden != null) {
onHidden.run();
}
}
}
private ListViewAdapter createFilterListAdapter(Set<String> enabledNotifications) {
List<ListItem> listItems = new ArrayList<>();
// Start with all notifications enabled
listItems.add(new GamedayTickerFilterCheckbox(R.layout.list_item_checkbox_upcoming_match, "Upcoming Matches", NotificationTypes.UPCOMING_MATCH, true));
listItems.add(new GamedayTickerFilterCheckbox(R.layout.list_item_checkbox_match_results, "Match Results", NotificationTypes.MATCH_SCORE, true));
listItems.add(new GamedayTickerFilterCheckbox(R.layout.list_item_checkbox_schedule_updated, "Schedule Updated", NotificationTypes.SCHEDULE_UPDATED, true));
listItems.add(new GamedayTickerFilterCheckbox(R.layout.list_item_checkbox_competition_level_starting, "Competition Level Starting", NotificationTypes.LEVEL_STARTING, true));
listItems.add(new GamedayTickerFilterCheckbox(R.layout.list_item_checkbox_alliance_selections, "Alliance Selections", NotificationTypes.ALLIANCE_SELECTION, true));
listItems.add(new GamedayTickerFilterCheckbox(R.layout.list_item_checkbox_awards_posted, "Awards Posted", NotificationTypes.AWARDS, true));
// Initialize the preferences to their appropriate value
if (enabledNotifications != null) {
for (ListItem item : listItems) {
GamedayTickerFilterCheckbox checkbox = (GamedayTickerFilterCheckbox) item;
if (!enabledNotifications.contains(checkbox.getKey())) {
checkbox.setChecked(false);
}
}
}
return new ListViewAdapter(getActivity(), listItems);
}
private ListViewAdapter createFilterListAdapter() {
return createFilterListAdapter(null);
}
private void loadFirebaseParams() {
//FIXME read these from FirebaseRemoteConfig
/*
String firebaseBase = Utilities.readLocalProperty(getActivity(), "firebase.url",
FIREBASE_URL_DEFAULT);
mFirebaseUrl = firebaseBase + getFirebaseUrlSuffix();
String loadDepthTemp = Utilities.readLocalProperty(getActivity(), "firebase.depth",
Integer.toString(FIREBASE_LOAD_DEPTH_DEFAULT));
try {
mFirebaseLoadDepth = Integer.parseInt(loadDepthTemp);
} catch (NumberFormatException e) {
mFirebaseLoadDepth = FIREBASE_LOAD_DEPTH_DEFAULT;
}*/
}
private void updateList() {
// Collect a list of all enabled notification keys
final Set<String> enabledNotificationKeys = getEnabledNotificationKeys();
Observable.from(mAllNotifications)
.filter(n -> n != null)
.filter(notification -> enabledNotificationKeys.contains(notification.getNotificationType()))
.map(notification -> {
notification.parseMessageData();
return notification.renderToViewModel(getContext(), null);
})
.toList()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationsList -> {
mAreFilteredNotificationsVisible = !notificationsList.isEmpty();
if (!FirebaseTickerFragment.this.isResumed() || getView() == null) {
return;
}
mNotificationsAdapter.animateTo(notificationsList);
// If we're at the top of the list, maintain our position so any new items
// above our current first item will animate into view
if (mNotificationsRecyclerView.computeVerticalScrollOffset() == 0) {
mNotificationsRecyclerView.scrollToPosition(0);
}
updateViewVisibility();
}, throwable -> {
TbaLogger.e("Firebase error");
throwable.printStackTrace();
// Show the "none found" warning
mAreFilteredNotificationsVisible = false;
updateViewVisibility();
});
}
private Set<String> getEnabledNotificationKeys() {
final Set<String> enabledNotificationKeys = new HashSet<>();
int filterItemCount = mFilterListView.getAdapter().getCount();
for (int i = 0; i < filterItemCount; i++) {
GamedayTickerFilterCheckbox checkbox = ((GamedayTickerFilterCheckbox) mFilterListView.getAdapter().getItem(i));
if (checkbox.isChecked()) {
enabledNotificationKeys.add(checkbox.getKey());
}
}
return enabledNotificationKeys;
}
public void call(List<FirebaseNotification> firebaseNotifications) {
if (firebaseNotifications.isEmpty()) {
return;
}
for (FirebaseNotification firebaseNotification : firebaseNotifications) {
mComponent.inject(firebaseNotification);
mAllNotifications.add(0, firebaseNotification.getNotification());
}
updateList();
}
public Mapper createAdapterMapper() {
Mapper mapper = new Mapper();
mapper.add(AllianceSelectionNotificationViewModel.class, AllianceSelectionNotificationItemView.class)
.add(AwardsPostedNotificationViewModel.class, AwardsPostedNotificationItemView.class)
.add(CompLevelStartingNotificationViewModel.class, CompLevelStartingNotificationItemView.class)
.add(UpcomingMatchNotificationViewModel.class, UpcomingMatchNotificationItemView.class)
.add(ScoreNotificationViewModel.class, ScoreNotificationItemView.class)
.add(ScheduleUpdatedNotificationViewModel.class, ScheduleUpdatedNotificationItemView.class)
.add(GenericNotificationViewModel.class, GenericNotificationItemView.class);
return mapper;
}
protected abstract void inject();
protected abstract String getFirebaseUrlSuffix();
private static final class ViewCrossfader {
private List<View> mFadeIn, mFadeOut;
private int mDuration;
private ViewCrossfader(int duration) {
mDuration = duration;
mFadeIn = new ArrayList<>();
mFadeOut = new ArrayList<>();
}
public static ViewCrossfader create(int duration) {
return new ViewCrossfader(duration);
}
public ViewCrossfader fadeIn(View v) {
mFadeIn.add(v);
return this;
}
public ViewCrossfader fadeOut(View v) {
mFadeOut.add(v);
return this;
}
public void start() {
for (View fadeOut : mFadeOut) {
fadeOut.animate()
.alpha(0.0f)
.setDuration(mDuration)
.setStartDelay(0)
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationEnd(Animator animation) {
fadeOut.setVisibility(View.GONE);
}
}).start();
}
for (View fadeIn : mFadeIn) {
fadeIn.animate()
.alpha(1.0f)
.setDuration(mDuration)
.setStartDelay(mDuration)
.setListener(new AnimatorListenerAdapter() {
@Override public void onAnimationStart(Animator animation) {
fadeIn.setVisibility(View.VISIBLE);
}
}).start();
}
}
}
}