package com.tevinjeffrey.rutgersct.ui.trackedsections; import android.app.FragmentTransaction; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.transition.Fade; import android.transition.Transition; import android.transition.TransitionInflater; 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.view.animation.OvershootInterpolator; import android.widget.TextView; import com.google.gson.JsonParseException; import com.nispok.snackbar.Snackbar; import com.nispok.snackbar.SnackbarManager; import com.nispok.snackbar.enums.SnackbarType; import com.nispok.snackbar.listeners.ActionClickListener; import com.nispok.snackbar.listeners.ActionSwipeListener; import com.nispok.snackbar.listeners.EventListener; import com.tevinjeffrey.rutgersct.R; import com.tevinjeffrey.rutgersct.RutgersCTApp; import com.tevinjeffrey.rutgersct.ui.utils.ItemClickListener; import com.tevinjeffrey.rutgersct.ui.utils.CircleSharedElementCallback; import com.tevinjeffrey.rutgersct.ui.utils.CircleView; import com.tevinjeffrey.rutgersct.rutgersapi.model.Course.Section; import com.tevinjeffrey.rutgersct.ui.base.MVPFragment; import com.tevinjeffrey.rutgersct.ui.chooser.ChooserFragment; import com.tevinjeffrey.rutgersct.ui.sectioninfo.SectionInfoFragment; import com.tevinjeffrey.rutgersct.ui.utils.RecyclerSimpleScrollListener; import com.tevinjeffrey.rutgersct.utils.Utils; import com.tevinjeffrey.rutgersct.rutgersapi.exceptions.RutgersServerIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; import icepick.Icicle; import rx.functions.Action1; import timber.log.Timber; import static com.tevinjeffrey.rutgersct.ui.base.View.LayoutType.EMPTY; import static com.tevinjeffrey.rutgersct.ui.base.View.LayoutType.LIST; @SuppressWarnings({"ClassWithTooManyMethods"}) public class TrackedSectionsFragment extends MVPFragment implements TrackedSectionsView, SwipeRefreshLayout.OnRefreshListener, ItemClickListener<Section, View> { public static final String TAG = TrackedSectionsFragment.class.getSimpleName(); @Bind(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout; @Bind(R.id.toolbar) Toolbar mToolbar; @Bind(R.id.add_courses_fab) FloatingActionButton mFab; @Bind(R.id.tsf_list) RecyclerView mRecyclerView; @Bind(R.id.add_courses_to_track) ViewGroup mEmptyView; @Bind(R.id.error_view) ViewGroup mErrorView; @Icicle TrackedSectionsViewState mViewState = new TrackedSectionsViewState(); private ArrayList<Section> mListDataset; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { LayoutInflater themedInflator = inflater.cloneInContext(Utils.wrapContextTheme(getActivity(), R.style.RutgersCT)); ViewGroup rootView = (ViewGroup) themedInflator.inflate(R.layout.fragment_tracked_sections, container, false); ButterKnife.bind(this, rootView); return rootView; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); //Recreate presenter if necessary. if (mBasePresenter == null) { mBasePresenter = new TrackedSectionsPresenterImpl(); //Field injection instead of constructor injection. Less code when adding dependancies. RutgersCTApp.getObjectGraph(getParentActivity()).inject(mBasePresenter); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mViewState.apply(this, savedInstanceState != null); //Attach view to presenter mBasePresenter.attachView(this); //Load data depending on if the view is currently refreshing if (mIsInitialLoad) { getPresenter().loadTrackedSections(true); } else { //Silently refresh tracked sections if (!getPresenter().isLoading()) { getPresenter().loadTrackedSections(false); } } } @Override public void onDestroyView() { super.onDestroyView(); ButterKnife.unbind(this); dismissSnackbar(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_tracked_sections, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_refresh: onRefresh(); return true; case R.id.action_webreg: launchWebReg(); return true; case R.id.action_rate: launchMarket(); return true; default: return super.onOptionsItemSelected(item); } } public void initToolbar() { setToolbar(mToolbar); } @Override public void showLoading(final boolean pullToRefresh) { mViewState.isRefreshing = pullToRefresh; mSwipeRefreshLayout.post(new Runnable() { @Override public void run() { if (mSwipeRefreshLayout != null) { mSwipeRefreshLayout.setRefreshing(pullToRefresh); } } }); } public void setData(List<Section> data) { mViewState.data = data; mListDataset.clear(); mListDataset.addAll(data); mRecyclerView.getAdapter().notifyDataSetChanged(); if (data.size() == 0) showLayout(EMPTY); else if (data.size() > 0) showLayout(LIST); } @Override public void showError(Throwable t) { String message; Resources resources = getContext().getResources(); if (t instanceof UnknownHostException) { message = resources.getString(R.string.no_internet); } else if (t instanceof JsonParseException || t instanceof RutgersServerIOException) { message = resources.getString(R.string.server_down); } else if (t instanceof SocketTimeoutException) { message = resources.getString(R.string.timed_out); } else { message = t.getMessage(); } // Save current error message ahead of config change. mViewState.errorMessage = message; // Show the error layout if there's nothing in the adpater to show. // Redirects the message that would usually be in the snackbar, to error layout. // https://www.google.com/design/spec/patterns/errors.html#errors-app-errors if (!adapterHasItems()) { showLayout(LayoutType.ERROR); TextView textViewMessage = ButterKnife.findById(mErrorView, R.id.text); textViewMessage.setText(message); } else { showSnackBar(message); } } public void showLayout(LayoutType type) { mViewState.layoutType = type; switch (type) { case ERROR: showEmptyLayout(View.GONE); showRecyclerView(View.GONE); showErrorLayout(View.VISIBLE); break; case EMPTY: showRecyclerView(View.GONE); showErrorLayout(View.GONE); showEmptyLayout(View.VISIBLE); break; case LIST: showErrorLayout(View.GONE); showEmptyLayout(View.GONE); showRecyclerView(View.VISIBLE); break; default: throw new RuntimeException("Unknown type: " + type); } } @OnClick(R.id.try_again) public void onTryAgainClick(View view) { onRefresh(); } private boolean adapterHasItems() { return mRecyclerView.getAdapter().getItemCount() > 0; } private void dismissSnackbar() { //It's only being dismissed to not leak the fragment if (SnackbarManager.getCurrentSnackbar() != null) { SnackbarManager.dismiss(); } } //Click events get through to the SwipeRefreshLayout even though another view is covering it. // Instead of interecepting clicks I can disable it the layout entirely. Unfortunately disabling the SRL disables // the gesture and the loading animation when setEnabled(false) is called. This is an issue as // the refresh animation is the only way to notify the user of the work being done. So e.g if the // we're in an empty state an the user issues a refresh there will be no refresh animation. /*private void enableSwipeRefreshLayout(boolean enable) { mSwipeRefreshLayout.setEnabled(enable); }*/ private void showErrorLayout(int visibility) { if (mErrorView.getVisibility() != visibility) mErrorView.setVisibility(visibility); } private void showEmptyLayout(int visibility) { if (mEmptyView.getVisibility() != visibility) mEmptyView.setVisibility(visibility); } private void showRecyclerView(int visibility) { if (mRecyclerView.getVisibility() != visibility) mRecyclerView.setVisibility(visibility); } public void initRecyclerView() { LinearLayoutManager layoutManager = new LinearLayoutManager(getParentActivity()); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); layoutManager.setSmoothScrollbarEnabled(true); mRecyclerView.setLayoutManager(layoutManager); mRecyclerView.setHasFixedSize(true); RecyclerSimpleScrollListener recyclerSimpleScrollListener = new RecyclerSimpleScrollListener(); mRecyclerView.addOnScrollListener(recyclerSimpleScrollListener); recyclerSimpleScrollListener.getDirectionObservable().subscribe(new Action1<RecyclerSimpleScrollListener.Direction>() { @Override public void call(RecyclerSimpleScrollListener.Direction direction) { switch (direction) { case UP: animateFabIn(); //These helper methods have a glitchy animation on some Samsung devices. //mFab.show(); break; case DOWN: animateFabOut(); //These helper methods have a glitchy animation on some Samsung devices. //mFab.hide(); break; case NEUTRAL: break; } } //The animation up and down takes into account if he snackbar is showing or not. private void animateFabIn() { ViewCompat.animate(mFab).alpha(1).setStartDelay(50).start(); ViewCompat.animate(mFab).translationY(mViewState.snackBarShowing? -SnackbarManager.getCurrentSnackbar().getHeight():0).setStartDelay(50).start(); } private void animateFabOut() { ViewCompat.animate(mFab).alphaBy(0).setStartDelay(50).start(); ViewCompat.animate(mFab).translationYBy(250).setStartDelay(50).start(); } }); if (mListDataset == null) { mListDataset = new ArrayList<>(10); } if (mRecyclerView.getAdapter() == null) { mRecyclerView.setAdapter(new TrackedSectionsFragmentAdapter(mListDataset, this)); } } public void initSwipeLayout() { mSwipeRefreshLayout.setOnRefreshListener(this); mSwipeRefreshLayout.setSize(SwipeRefreshLayout.DEFAULT); mSwipeRefreshLayout.setColorSchemeResources(R.color.red, R.color.green); } @OnClick(R.id.add_courses_fab) public void onFabClick(View view) { startChooserFragment(); } @Override public void onRefresh() { // Retrieve section and while showing the loading animation. getPresenter().loadTrackedSections(true); } @Override public void onItemClicked(Section section, View view) { Timber.i("Selected tracked section: %s", section); startSectionInfoFragment(SectionInfoFragment.newInstance(section), view); } private void showSnackBar(CharSequence message) { // TODO Repace when android.design Snackbar recieves decent callbacks. SnackbarManager.show( Snackbar.with(getParentActivity()) .type(SnackbarType.MULTI_LINE) .text(message) .actionLabel("RETRY")// text to display .actionListener(new ActionClickListener() { @Override public void onActionClicked(Snackbar snackbar) { onRefresh(); mViewState.snackBarShowing = false; } }) .swipeListener(new ActionSwipeListener() { @Override public void onSwipeToDismiss() { mViewState.snackBarShowing = false; } }) .actionColor(ContextCompat.getColor(getParentActivity(), android.R.color.white)) .color(ContextCompat.getColor(getParentActivity(), R.color.accent))// action button label color .duration(Snackbar.SnackbarDuration.LENGTH_INDEFINITE) .eventListener(new EventListener() { @Override public void onShow(Snackbar snackbar) { if (snackbar != null) { mViewState.snackBarShowing = true; if (mFab != null) { ViewCompat.animate(mFab).translationYBy(-snackbar.getHeight()).setInterpolator(new OvershootInterpolator()); } } } @Override public void onShowByReplace(Snackbar snackbar) { } @Override public void onShown(Snackbar snackbar) { } @Override public void onDismiss(Snackbar snackbar) { if (mFab != null) { ViewCompat.animate(mFab).translationYBy(snackbar.getHeight()).setInterpolator(new OvershootInterpolator()); } } @Override public void onDismissByReplace(Snackbar snackbar) { } @Override public void onDismissed(Snackbar snackbar) { } }) // Snackbar's EventListener , getParentActivity()); // activity where it is displayed } private void startChooserFragment() { ChooserFragment chooserFragment = ChooserFragment.newInstance(); FragmentTransaction ft = getFragmentManager().beginTransaction(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mToolbar.setTransitionName(getString(R.string.transition_name_tool_background)); ft.addSharedElement(mToolbar, getString(R.string.transition_name_tool_background)); setExitTransition(new Fade(Fade.OUT).setDuration(getResources().getInteger(R.integer.exit_anim))); chooserFragment.setAllowEnterTransitionOverlap(false); chooserFragment.setAllowReturnTransitionOverlap(false); } else { ft.setCustomAnimations(R.anim.enter, R.anim.exit, R.anim.pop_enter, R.anim.pop_exit); } startFragment(TrackedSectionsFragment.this, chooserFragment, ft); } private void startSectionInfoFragment(SectionInfoFragment sectionInfoFragment, View view) { FragmentTransaction ft = this.getFragmentManager().beginTransaction(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CircleView circleView = ButterKnife.findById(view, R.id.section_number_background); mFab.setTransitionName(getString(R.string.transition_name_fab)); ft.addSharedElement(mFab, getString(R.string.transition_name_fab)); circleView.setTransitionName(getString(R.string.transition_name_circle_view)); ft.addSharedElement(circleView, getString(R.string.transition_name_circle_view)); Transition tsfSectionEnter = TransitionInflater .from(getParentActivity()) .inflateTransition(R.transition.tsf_section_enter); Transition tsfSectionReturn = TransitionInflater .from(getParentActivity()) .inflateTransition(R.transition.tsf_section_return); sectionInfoFragment.setEnterTransition(tsfSectionEnter); sectionInfoFragment.setReturnTransition(tsfSectionReturn); setReenterTransition(new Fade(Fade.IN).addTarget(RecyclerView.class)); setExitTransition(new Fade(Fade.OUT).addTarget(RecyclerView.class)); sectionInfoFragment.setAllowReturnTransitionOverlap(false); sectionInfoFragment.setAllowEnterTransitionOverlap(false); Transition sharedElementsEnter = TransitionInflater .from(getParentActivity()) .inflateTransition(R.transition.tsf_shared_element_enter); Transition sharedElementsReturn = TransitionInflater .from(getParentActivity()) .inflateTransition(R.transition.tsf_shared_element_return); sectionInfoFragment.setSharedElementEnterTransition(sharedElementsEnter); sectionInfoFragment.setSharedElementReturnTransition(sharedElementsReturn); CircleSharedElementCallback sharedelementCallback = new CircleSharedElementCallback(); sectionInfoFragment.setEnterSharedElementCallback(sharedelementCallback); sharedElementsEnter.addListener(sharedelementCallback.getTransitionCallback()); } else { ft.setCustomAnimations(R.anim.enter, R.anim.exit, R.anim.pop_enter, R.anim.pop_exit); } startFragment(this, sectionInfoFragment, ft); } private void launchWebReg() { String url = "http://webreg.rutgers.edu"; Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } private void launchMarket() { final Uri uri = Uri.parse("market://details?id=" + getParentActivity().getApplicationContext().getPackageName()); final Intent rateAppIntent = new Intent(Intent.ACTION_VIEW, uri); if (getParentActivity().getPackageManager().queryIntentActivities(rateAppIntent, 0).size() > 0) { startActivity(rateAppIntent); } } private TrackedSectionsPresenter getPresenter() { return (TrackedSectionsPresenter) mBasePresenter; } @Override public String toString() { return TAG; } }