package com.thebluealliance.androidclient.datafeed.refresh; import com.thebluealliance.androidclient.R; import android.support.annotation.IntDef; import android.support.annotation.UiThread; import android.support.v4.util.ArrayMap; import android.view.MenuItem; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collection; import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; /** * Provides an easy way to manage a collection of {@link Refreshable} objects. {@link Refreshable} * objects should register themselves with this class; they will receive callbacks when a refresh * is requested. Additionally, they can push their current state (refreshing or not) into this * class. This allows us to determine if any registered objects are currently refreshing and take * the appropriate action. * <p> * The primary way this class will be used is to work with a {@link MenuItem} that controls * refreshing. When you bind a {@link RefreshController} to a {@link MenuItem} with the {@link * RefreshController#bindToMenuItem(MenuItem)} method, this class will monitor the state of its * registered {@link Refreshable}s and replace the item's action view with a progress indicator and * revert the icon to its normal state when the refresh is finished. And, as long as you proxy * {@link android.app.Activity#onOptionsItemSelected(MenuItem)} calls through to this class, it * will automatically start a refresh when the bound {@link MenuItem} is clicked. */ @Singleton public class RefreshController { /** * Constants for refresh type */ @Retention(RetentionPolicy.SOURCE) @IntDef({REQUESTED_BY_USER, NOT_REQUESTED_BY_USER}) public @interface RefreshType {} public static final int REQUESTED_BY_USER = 0; public static final int NOT_REQUESTED_BY_USER = 1; /** * Maps refresh tags to {@link Refreshable} objects and their current refreshing state */ private Map<String, RefreshWrapper> mRefreshableStates; /** * Optional listener that will receive a callback when the refreshing state changes */ private RefreshStateListener mListener; /** * Optional {@link MenuItem} that is bound to */ private MenuItem mMenuItem; /** * True if any registered {@link Refreshable}s are refreshing, false if none are */ private boolean mIsRefreshing = false; @Inject public RefreshController() { mRefreshableStates = new ArrayMap<>(); } /** * Attaches a listener that will receive a callback when the refresh state changes * * @param listener listener to receive callbacks */ public void setRefreshStateListener(RefreshStateListener listener) { mListener = listener; } /** * Binding to a {@link }MenuItem} will automatically replace its action view with a loading * indicator when a refresh is in progress. * * @param menuItem the {@link MenuItem} to bind to */ public void bindToMenuItem(MenuItem menuItem) { if (menuItem == null) { return; } mMenuItem = menuItem; updateMenuItemState(); } /** * Calls to {@link android.app.Activity#onOptionsItemSelected(MenuItem)} should be proxied * through to this method if you want to take advantage of automatically starting a refresh * when the bound {@link MenuItem} is clicked. * * @param menuItem the {@link MenuItem} that was clicked. * @return true if the click was handled, false otherwise */ public boolean onOptionsItemSelected(MenuItem menuItem) { if (mMenuItem == null) { return false; } if (menuItem.getItemId() == mMenuItem.getItemId()) { // Refresh button clicked, start refresh startRefresh(REQUESTED_BY_USER); // Click handled return true; } return false; } public void registerRefreshable(String refreshTag, Refreshable refreshable) { // Default to "not refreshing" mRefreshableStates.put(refreshTag, new RefreshWrapper(refreshable, false)); } public void unregisterRefreshable(String refreshTag) { mRefreshableStates.remove(refreshTag); } /** * Simply calls {@link Refreshable#onRefreshStart(int)} on all registered {@link * Refreshable}s. */ public void startRefresh(@RefreshType int refreshType) { if (mIsRefreshing) { return; } if (mRefreshableStates.isEmpty()) { return; } mIsRefreshing = true; for (RefreshWrapper wrapper : mRefreshableStates.values()) { Refreshable refreshable = wrapper.getRefreshable(); if (refreshable != null) { refreshable.onRefreshStart(refreshType); } } } /** * Called to notify this class that a {@link Refreshable}'s refreshing state has changed. Note * that this will also register the {@link Refreshable} for future refresh start callbacks if * it was not already registered. * * @param refreshKey the String linking to the {@link Refreshable} object being updated * @param isRefreshing true if the {@link Refreshable} is currently refreshing, false if it is * not */ @UiThread public void notifyRefreshingStateChanged(String refreshKey, boolean isRefreshing) { RefreshWrapper wrapper = mRefreshableStates.get(refreshKey); if (wrapper == null) { return; } wrapper.setRefreshState(isRefreshing); boolean oldRefreshingState = mIsRefreshing; updateRefreshingState(); updateMenuItemState(); if (mIsRefreshing != oldRefreshingState) { // The state changed. Notify the listener, if one exists if (mListener != null) { mListener.onRefreshStateChanged(mIsRefreshing); } } } /** * Resets the state of the instance */ public void reset() { mIsRefreshing = false; mRefreshableStates.clear(); } /** * Checks to see if any {@link Refreshable}s are refreshing, and updates an internal flag with * the result. * * @return returns the value of {@code mIsRefreshing} for convenience */ @UiThread private boolean updateRefreshingState() { Collection<RefreshWrapper> refreshingStates = mRefreshableStates.values(); for (RefreshWrapper wrapper : refreshingStates) { if (wrapper.getRefreshState()) { mIsRefreshing = true; return true; } } mIsRefreshing = false; return false; } /** * Based on the current refreshing state, shows or hides a loading indicator from the bound * {@link MenuItem}, if applicable. */ @UiThread private void updateMenuItemState() { if (mMenuItem == null) { return; } boolean isMenuProgressShowing = (mMenuItem.getActionView() != null); if (!mIsRefreshing && isMenuProgressShowing) { // Hide progress indicator mMenuItem.setActionView(null); } else if (mIsRefreshing && !isMenuProgressShowing) { // Show progress indicator mMenuItem.setActionView(R.layout.actionbar_indeterminate_progress); } } }