package com.gordonwong.materialsheetfab.animations;

import java.lang.reflect.Method;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.os.Build;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.animation.Interpolator;

import com.gordonwong.materialsheetfab.MaterialSheetFab.RevealXDirection;
import com.gordonwong.materialsheetfab.MaterialSheetFab.RevealYDirection;

import io.codetail.animation.SupportAnimator;

/**
 * Created by Gordon Wong on 7/5/2015.
 *
 * Animates the material sheet into and out of view.
 */
public class MaterialSheetAnimation {

	private static final String SUPPORT_CARDVIEW_CLASSNAME = "android.support.v7.widget.CardView";
	private static final int SHEET_REVEAL_OFFSET_Y = 5;

	private View sheet;
	private int sheetColor;
	private int fabColor;
	private Interpolator interpolator;
	private RevealXDirection revealXDirection;
	private RevealYDirection revealYDirection;
	private Method setCardBackgroundColor;
	private boolean isSupportCardView;

	public MaterialSheetAnimation(View sheet, int sheetColor, int fabColor,
			Interpolator interpolator) {
		this.sheet = sheet;
		this.sheetColor = sheetColor;
		this.fabColor = fabColor;
		this.interpolator = interpolator;
		// Default reveal direction is up and to the left (for FABs in the bottom right corner)
		revealXDirection = RevealXDirection.LEFT;
		revealYDirection = RevealYDirection.UP;
		isSupportCardView = sheet.getClass().getName().equals(SUPPORT_CARDVIEW_CLASSNAME);
		// Get setCardBackgroundColor() method
		if (isSupportCardView) {
			try {
				// noinspection unchecked
				setCardBackgroundColor = sheet.getClass()
						.getDeclaredMethod("setCardBackgroundColor", int.class);
			} catch (Exception e) {
				setCardBackgroundColor = null;
			}
		}
	}

	/**
	 * Aligns the sheet's position with the FAB.
	 *
	 * @param fab Floating action button
	 */
	public void alignSheetWithFab(View fab) {
		// NOTE: View.getLocationOnScreen() returns the view's coordinates on the screen
		// whereas other view methods like getRight() and getY() return coordinates relative
		// to the view's parent. Using those methods can lead to incorrect calculations when
		// the two views do not have the same parent.

		// Get FAB's coordinates
		int[] fabCoords = new int[2];
		fab.getLocationOnScreen(fabCoords);

		// Get sheet's coordinates
		int[] sheetCoords = new int[2];
		sheet.getLocationOnScreen(sheetCoords);

		// NOTE: Use the diffs between the positions of the FAB and sheet to align the sheet.
		// We have to use the diffs because the coordinates returned by getLocationOnScreen()
		// include the status bar and any other system UI elements, meaning the coordinates
		// aren't representative of the usable screen space.
		int leftDiff = sheetCoords[0] - fabCoords[0];
		int rightDiff = (sheetCoords[0] + sheet.getWidth()) - (fabCoords[0] + fab.getWidth());
		int topDiff = sheetCoords[1] - fabCoords[1];
		int bottomDiff = (sheetCoords[1] + sheet.getHeight()) - (fabCoords[1] + fab.getHeight());

		// NOTE: Preserve the sheet's margins to allow users to shift the sheet's position
		// to compensate for the fact that the design support library's FAB has extra
		// padding within the view
		ViewGroup.MarginLayoutParams sheetLayoutParams = (ViewGroup.MarginLayoutParams) sheet
				.getLayoutParams();

		// Set sheet's new coordinates (only if there is a change in coordinates because
		// setting the same coordinates can cause the view to "drift" - moving 0.5 to 1 pixels
		// around the screen)
		if (rightDiff != 0) {
			float sheetX = sheet.getX();
			// Align the right side of the sheet with the right side of the FAB if
			// doing so would not move the sheet off the screen
			if (rightDiff <= sheetX) {
				sheet.setX(sheetX - rightDiff - sheetLayoutParams.rightMargin);
				revealXDirection = RevealXDirection.LEFT;
			}
			// Otherwise, align the left side of the sheet with the left side of the FAB
			else if (leftDiff != 0 && leftDiff <= sheetX) {
				sheet.setX(sheetX - leftDiff + sheetLayoutParams.leftMargin);
				revealXDirection = RevealXDirection.RIGHT;
			}
		}

		if (bottomDiff != 0) {
			float sheetY = sheet.getY();
			// Align the bottom of the sheet with the bottom of the FAB
			if (bottomDiff <= sheetY) {
				sheet.setY(sheetY - bottomDiff - sheetLayoutParams.bottomMargin);
				revealYDirection = RevealYDirection.UP;
			}
			// Otherwise, align the top of the sheet with the top of the FAB
			else if (topDiff != 0 && topDiff <= sheetY) {
				sheet.setY(sheetY - topDiff + sheetLayoutParams.topMargin);
				revealYDirection = RevealYDirection.DOWN;
			}
		}
	}

	/**
	 * Shows the sheet by morphing the FAB into the sheet.
	 *
	 * @param fab Floating action button
	 * @param showSheetDuration Duration of the sheet animation in milliseconds. Use 0 for no
	 *            animation.
	 * @param showSheetColorDuration Duration of the color animation in milliseconds. Use 0 for no
	 *            animation.
	 * @param listener Listener for animation events.
	 */
	public void morphFromFab(View fab, long showSheetDuration, long showSheetColorDuration,
			AnimationListener listener) {
		sheet.setVisibility(View.VISIBLE);
		revealSheetWithFab(fab, getFabRevealRadius(fab), getSheetRevealRadius(), showSheetDuration,
				fabColor, sheetColor, showSheetColorDuration, listener);
	}

	/**
	 * Hides the sheet by morphing the sheet into the FAB.
	 *
	 * @param fab Floating action button
	 * @param hideSheetDuration Duration of the sheet animation in milliseconds. Use 0 for no
	 *            animation.
	 * @param hideSheetColorDuration Duration of the color animation in milliseconds. Use 0 for no
	 *            animation.
	 * @param listener Listener for animation events.
	 */
	public void morphIntoFab(View fab, long hideSheetDuration, long hideSheetColorDuration,
			AnimationListener listener) {
		revealSheetWithFab(fab, getSheetRevealRadius(), getFabRevealRadius(fab), hideSheetDuration,
				sheetColor, fabColor, hideSheetColorDuration, listener);
	}

	protected void revealSheetWithFab(View fab, float startRadius, float endRadius,
			long sheetDuration, int startColor, int endColor, long sheetColorDuration,
			AnimationListener listener) {
		if (listener != null) {
			listener.onStart();
		}

		// Pass listener to the animation that will be the last to finish
		AnimationListener revealListener = (sheetDuration >= sheetColorDuration) ? listener : null;
		AnimationListener colorListener = (sheetColorDuration > sheetDuration) ? listener : null;

		// Start animations
		startCircularRevealAnim(sheet, getSheetRevealCenterX(), getSheetRevealCenterY(fab),
				startRadius, endRadius, sheetDuration, interpolator, revealListener);
		startColorAnim(sheet, startColor, endColor, sheetColorDuration, interpolator,
				colorListener);
	}

	protected void startCircularRevealAnim(View view, int centerX, int centerY, float startRadius,
			float endRadius, long duration, Interpolator interpolator,
			final AnimationListener listener) {
		// Use native circular reveal on Android 5.0+
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
			// Native circular reveal uses coordinates relative to the view
			int relativeCenterX = (int) (centerX - view.getX());
			int relativeCenterY = (int) (centerY - view.getY());
			// Setup animation
			Animator anim = ViewAnimationUtils.createCircularReveal(view, relativeCenterX,
					relativeCenterY, startRadius, endRadius);
			anim.setDuration(duration);
			anim.setInterpolator(interpolator);
			// Add listener
			anim.addListener(new AnimatorListenerAdapter() {
				@Override
				public void onAnimationStart(Animator animation) {
					if (listener != null) {
						listener.onStart();
					}
				}

				@Override
				public void onAnimationEnd(Animator animation) {
					if (listener != null) {
						listener.onEnd();
					}
				}
			});
			// Start animation
			anim.start();
		} else {
			// Circular reveal library uses absolute coordinates
			// Setup animation
			SupportAnimator anim = io.codetail.animation.ViewAnimationUtils
					.createCircularReveal(view, centerX, centerY, startRadius, endRadius);
			anim.setDuration((int) duration);
			anim.setInterpolator(interpolator);
			// Add listener
			anim.addListener(new SupportAnimator.SimpleAnimatorListener() {
				@Override
				public void onAnimationStart() {
					if (listener != null) {
						listener.onStart();
					}
				}

				@Override
				public void onAnimationEnd() {
					if (listener != null) {
						listener.onEnd();
					}
				}
			});
			// Start animation
			anim.start();
		}
	}

	protected void startColorAnim(final View view, final int startColor, final int endColor,
			long duration, Interpolator interpolator, final AnimationListener listener) {
		// Setup animation
		ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), startColor, endColor);
		anim.setDuration(duration);
		anim.setInterpolator(interpolator);
		// Add listeners
		anim.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationStart(Animator animation) {
				if (listener != null) {
					listener.onStart();
				}
			}

			@Override
			public void onAnimationEnd(Animator animation) {
				if (listener != null) {
					listener.onEnd();
				}
			}
		});
		anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator animator) {
				// Update background color
				Integer color = (Integer) animator.getAnimatedValue();

				// Use CardView.setCardBackgroundColor() to avoid crashes on Android < 5.0 and to
				// properly set the card's background color without removing the card's other styles
				// See https://github.com/gowong/material-sheet-fab/pull/2 and
				// https://code.google.com/p/android/issues/detail?id=77843
				if (isSupportCardView) {
					// Use setCardBackground() method if it is available
					if (setCardBackgroundColor != null) {
						try {
							setCardBackgroundColor.invoke(sheet, color);
						} catch (Exception e) {
							// Ignore exceptions since there's no other way set a support CardView's
							// background color
						}
					}
				}
				// Set background color for all other views
				else {
					view.setBackgroundColor(color);
				}
			}
		});
		// Start animation
		anim.start();
	}

	public void setSheetVisibility(int visibility) {
		sheet.setVisibility(visibility);
	}

	public boolean isSheetVisible() {
		return sheet.getVisibility() == View.VISIBLE;
	}

	/**
	 * @return Sheet reveal's center X coordinate
	 */
	public int getSheetRevealCenterX() {
		return (int) (sheet.getX() + (sheet.getWidth() / 2));
	}

	/**
	 * @return Sheet reveal's center Y coordinate
	 */
	public int getSheetRevealCenterY(View fab) {
		if (revealYDirection == RevealYDirection.UP) {
			return (int) (sheet.getY()
					+ (sheet.getHeight() * (SHEET_REVEAL_OFFSET_Y - 1) / SHEET_REVEAL_OFFSET_Y)
					- (fab.getHeight() / 2));
		}
		return (int) (sheet.getY() + (sheet.getHeight() / SHEET_REVEAL_OFFSET_Y)
				+ (fab.getHeight() / 2));
	}

	protected float getSheetRevealRadius() {
		return Math.max(sheet.getWidth(), sheet.getHeight());
	}

	protected float getFabRevealRadius(View fab) {
		return Math.max(fab.getWidth(), fab.getHeight()) / 2;
	}

	public RevealXDirection getRevealXDirection() {
		return revealXDirection;
	}

	public RevealYDirection getRevealYDirection() {
		return revealYDirection;
	}
}


package com.gordonwong.materialsheetfab.animations;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
import android.view.animation.Interpolator;

/**
 * Created by Gordon Wong on 7/9/2015.
 *
 * Animates the overlay when showing and hiding the material sheet.
 */
public class OverlayAnimation {

	protected View overlay;
	protected Interpolator interpolator;

	public OverlayAnimation(View overlay, Interpolator interpolator) {
		this.overlay = overlay;
		this.interpolator = interpolator;
	}

	/**
	 * Shows the overlay.
	 *
	 * @param duration Duration of the animation in milliseconds. Use 0 for no animation.
	 * @param listener Listener for animation events.
	 */
	public void show(long duration, final AnimationListener listener) {
		overlay.animate().alpha(1).setDuration(duration).setInterpolator(interpolator)
				.setListener(new AnimatorListenerAdapter() {
					@Override
					public void onAnimationStart(Animator animation) {
						overlay.setVisibility(View.VISIBLE);
						if (listener != null) {
							listener.onStart();
						}
					}

					@Override
					public void onAnimationEnd(Animator animation) {
						if (listener != null) {
							listener.onEnd();
						}
					}
				}).start();
	}

	/**
	 * Hides the overlay.
	 *
	 * @param duration Duration of the animation in milliseconds. Use 0 for no animation.
	 * @param listener Listener for animation events.
	 */
	public void hide(long duration, final AnimationListener listener) {
		overlay.animate().alpha(0).setDuration(duration).setInterpolator(interpolator)
				.setListener(new AnimatorListenerAdapter() {
					@Override
					public void onAnimationStart(Animator animation) {
						if (listener != null) {
							listener.onStart();
						}
					}

					@Override
					public void onAnimationEnd(Animator animation) {
						overlay.setVisibility(View.GONE);
						if (listener != null) {
							listener.onEnd();
						}
					}
				}).start();
	}

}


package com.gordonwong.materialsheetfab;

import android.os.Build;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;

import com.gordonwong.materialsheetfab.animations.AnimationListener;
import com.gordonwong.materialsheetfab.animations.FabAnimation;
import com.gordonwong.materialsheetfab.animations.MaterialSheetAnimation;
import com.gordonwong.materialsheetfab.animations.OverlayAnimation;

import io.codetail.animation.arcanimator.Side;

/**
 * Created by Gordon Wong on 7/9/2015.
 *
 * Handles the interactions between the FAB and the material sheet that the FAB morphs into.
 */
public class MaterialSheetFab<FAB extends View & AnimatedFab> {

	// TODO (7/9/15): Remove platform-specific constants (currently needed because of the different
	// interpolators used)
	private static final boolean IS_LOLLIPOP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;

	private static final int ANIMATION_SPEED = 1;

	// Animation durations
	private static final int SHEET_ANIM_DURATION = (IS_LOLLIPOP ? 600 : 300) * ANIMATION_SPEED;
	private static final int SHOW_SHEET_COLOR_ANIM_DURATION = (int) (SHEET_ANIM_DURATION * 0.75);
	private static final int HIDE_SHEET_COLOR_ANIM_DURATION = IS_LOLLIPOP
			? (int) (SHEET_ANIM_DURATION * 1.5) : (SHEET_ANIM_DURATION * 2);
	private static final int FAB_ANIM_DURATION = 300 * ANIMATION_SPEED;
	private static final int SHOW_OVERLAY_ANIM_DURATION = MaterialSheetFab.SHOW_SHEET_ANIM_DELAY
			+ SHEET_ANIM_DURATION;
	private static final int HIDE_OVERLAY_ANIM_DURATION = SHEET_ANIM_DURATION;

	// Animation delays
	private static final int SHOW_SHEET_ANIM_DELAY = (int) (FAB_ANIM_DURATION * 0.5);
	private static final int MOVE_FAB_ANIM_DELAY = IS_LOLLIPOP ? (int) (SHEET_ANIM_DURATION * 0.3)
			: (int) (SHEET_ANIM_DURATION * 0.6);

	// Other animation constants
	private static final float FAB_SCALE_FACTOR = 0.6f;
	private static final int FAB_ARC_DEGREES = 0;

	// Views
	protected FAB fab;

	// Animations
	protected FabAnimation fabAnimation;
	protected MaterialSheetAnimation sheetAnimation;
	protected OverlayAnimation overlayAnimation;

	// State
	protected int anchorX;
	protected int anchorY;
	private boolean isShowing;
	private boolean isHiding;
	private boolean hideSheetAfterSheetIsShown;

	// Listeners
	private MaterialSheetFabEventListener eventListener;

	public enum RevealXDirection {
		LEFT, RIGHT
	}

	public enum RevealYDirection {
		UP, DOWN
	}

	/**
	 * Creates a MaterialSheetFab instance and sets up the necessary click listeners.
	 *
	 * @param fab The FAB view.
	 * @param sheet The sheet view.
	 * @param overlay The overlay view.
	 * @param sheetColor The background color of the material sheet.
	 * @param fabColor The background color of the FAB.
	 */
	public MaterialSheetFab(FAB fab, View sheet, View overlay, int sheetColor, int fabColor) {
		Interpolator interpolator = AnimationUtils.loadInterpolator(sheet.getContext(),
				R.interpolator.msf_interpolator);

		this.fab = fab;

		// Create animations
		fabAnimation = new FabAnimation(fab, interpolator);
		sheetAnimation = new MaterialSheetAnimation(sheet, sheetColor, fabColor, interpolator);
		overlayAnimation = new OverlayAnimation(overlay, interpolator);

		// Set initial visibilities
		sheet.setVisibility(View.INVISIBLE);
		overlay.setVisibility(View.GONE);

		// Set listener to morph FAB into sheet when clicked
		fab.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				showSheet();
			}
		});

		// Set listener to hide the sheet when touching the overlay
		overlay.setOnTouchListener(new View.OnTouchListener() {
			@Override
			public boolean onTouch(View view, MotionEvent motionEvent) {
				// Only hide if the sheet is visible and if this is the first touch event
				if (isSheetVisible() && motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
					hideSheet();
				}
				return true;
			}
		});

		// Set listener for when FAB view is laid out
		fab.getViewTreeObserver()
				.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
					@Override
					public void onGlobalLayout() {
						// Remove listener so that this is only called once
						MaterialSheetFab.this.fab.getViewTreeObserver()
								.removeGlobalOnLayoutListener(this);
						// Initialize FAB anchor when the FAB view is laid out
						updateFabAnchor();
					}
				});
	}

	/**
	 * Shows the FAB.
	 */
	public void showFab() {
		showFab(0, 0);
	}

	/**
	 * Shows the FAB and sets the FAB's translation.
	 *
	 * @param translationX translation X value
	 * @param translationY translation Y value
	 */
	public void showFab(float translationX, float translationY) {
		// Update FAB's anchor
		setFabAnchor(translationX, translationY);
		// Show the FAB in the new position if the sheet is not visible
		if (!isSheetVisible()) {
			fab.show(translationX, translationY);
		}
	}

	/**
	 * Shows the sheet.
	 */
	public void showSheet() {
		if (isAnimating()) {
			return;
		}
		isShowing = true;

		// Show overlay
		overlayAnimation.show(SHOW_OVERLAY_ANIM_DURATION, null);

		// Morph FAB into sheet
		morphIntoSheet(new AnimationListener() {
			@Override
			public void onEnd() {
				// Call event listener
				if (eventListener != null) {
					eventListener.onSheetShown();
				}

				// Assuming that this is the last animation to finish
				isShowing = false;

				// Hide sheet after it is shown
				if (hideSheetAfterSheetIsShown) {
					hideSheet();
					hideSheetAfterSheetIsShown = false;
				}
			}
		});

		// Call event listener
		if (eventListener != null) {
			eventListener.onShowSheet();
		}
	}

	/**
	 * Hides the sheet.
	 */
	public void hideSheet() {
		hideSheet(null);
	}

	protected void hideSheet(final AnimationListener endListener) {
		if (isAnimating()) {
			// Wait until the sheet is shown and then hide it
			if (isShowing) {
				hideSheetAfterSheetIsShown = true;
			}
			return;
		}
		isHiding = true;

		// Hide overlay
		overlayAnimation.hide(HIDE_OVERLAY_ANIM_DURATION, null);

		// Morph FAB from sheet
		morphFromSheet(new AnimationListener() {
			@Override
			public void onEnd() {
				// Call event listeners
				if (endListener != null) {
					endListener.onEnd();
				}
				if (eventListener != null) {
					eventListener.onSheetHidden();
				}

				// Assuming that this is the last animation to finish
				isHiding = false;
			}
		});

		// Call event listener
		if (eventListener != null) {
			eventListener.onHideSheet();
		}
	}

	/**
	 * Hides the sheet (if visible) and then hides the FAB.
	 */
	public void hideSheetThenFab() {
		AnimationListener listener = new AnimationListener() {
			@Override
			public void onEnd() {
				fab.hide();
			}
		};
		// Hide sheet then hide FAB
		if (isSheetVisible()) {
			hideSheet(listener);
		}
		// Hide FAB
		else {
			listener.onEnd();
		}
	}

	protected void morphIntoSheet(final AnimationListener endListener) {

		// Update FAB anchor to ensure that the FAB returns to the correct position when hiding the
		// sheet
		updateFabAnchor();

		// Align sheet's position with FAB
		sheetAnimation.alignSheetWithFab(fab);

		// Morph FAB into sheet
		fabAnimation.morphIntoSheet(sheetAnimation.getSheetRevealCenterX(),
				sheetAnimation.getSheetRevealCenterY(fab),
				getFabArcSide(sheetAnimation.getRevealXDirection()), FAB_ARC_DEGREES,
				FAB_SCALE_FACTOR, FAB_ANIM_DURATION, null);

		// Show sheet after a delay
		new Handler().postDelayed(new Runnable() {
			@Override
			public void run() {
				// Hide FAB
				fab.setVisibility(View.INVISIBLE);

				// Show sheet
				sheetAnimation.morphFromFab(fab, SHEET_ANIM_DURATION,
						SHOW_SHEET_COLOR_ANIM_DURATION, endListener);
			}
		}, SHOW_SHEET_ANIM_DELAY);
	}

	protected void morphFromSheet(final AnimationListener endListener) {
		// Morph sheet into FAB
		sheetAnimation.morphIntoFab(fab, SHEET_ANIM_DURATION, HIDE_SHEET_COLOR_ANIM_DURATION, null);

		// Show FAB after a delay
		new Handler().postDelayed(new Runnable() {
			@Override
			public void run() {
				// Hide sheet
				sheetAnimation.setSheetVisibility(View.INVISIBLE);

				// Show FAB
				fabAnimation.morphFromSheet(anchorX, anchorY,
						getFabArcSide(sheetAnimation.getRevealXDirection()), FAB_ARC_DEGREES,
						-FAB_SCALE_FACTOR, FAB_ANIM_DURATION, endListener);
			}
		}, MOVE_FAB_ANIM_DELAY);
	}

	protected void updateFabAnchor() {
		// Update the anchor with the current translation
		setFabAnchor(fab.getTranslationX(), fab.getTranslationY());
	}

	protected void setFabAnchor(float translationX, float translationY) {
		anchorX = Math
				.round(fab.getX() + (fab.getWidth() / 2) + (translationX - fab.getTranslationX()));
		anchorY = Math
				.round(fab.getY() + (fab.getHeight() / 2) + (translationY - fab.getTranslationY()));
	}

	private Side getFabArcSide(RevealXDirection revealXDirection) {
		if (revealXDirection == RevealXDirection.LEFT) {
			return Side.LEFT;
		} else {
			return Side.RIGHT;
		}
	}

	private boolean isAnimating() {
		return isShowing || isHiding;
	}

	public boolean isSheetVisible() {
		return sheetAnimation.isSheetVisible();
	}

	public void setEventListener(MaterialSheetFabEventListener eventListener) {
		this.eventListener = eventListener;
	}

}


package com.gordonwong.materialsheetfab.sample;

import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

import com.gordonwong.materialsheetfab.MaterialSheetFab;
import com.gordonwong.materialsheetfab.MaterialSheetFabEventListener;
import com.gordonwong.materialsheetfab.sample.adapters.MainPagerAdapter;

/**
 * Created by Gordon Wong on 7/17/2015.
 *
 * Main activity for material sheet fab sample.
 */
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

	private ActionBarDrawerToggle drawerToggle;
	private DrawerLayout drawerLayout;
	private MaterialSheetFab materialSheetFab;
	private int statusBarColor;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setTitle(R.string.notes);
		setContentView(R.layout.activity_main);
		setupActionBar();
		setupDrawer();
		setupFab();
		setupTabs();
	}

	@Override
	protected void onPostCreate(Bundle savedInstanceState) {
		super.onPostCreate(savedInstanceState);
		drawerToggle.syncState();
	}

	@Override
	public void onBackPressed() {
		if (materialSheetFab.isSheetVisible()) {
			materialSheetFab.hideSheet();
		} else {
			super.onBackPressed();
		}
	}

	/**
	 * Sets up the action bar.
	 */
	private void setupActionBar() {
		setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
	}

	/**
	 * Sets up the navigation drawer.
	 */
	private void setupDrawer() {
		drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
		drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.opendrawer,
				R.string.closedrawer);
		drawerLayout.setDrawerListener(drawerToggle);
	}

	/**
	 * Sets up the tabs.
	 */
	private void setupTabs() {
		// Setup view pager
		ViewPager viewpager = (ViewPager) findViewById(R.id.viewpager);
		viewpager.setAdapter(new MainPagerAdapter(this, getSupportFragmentManager()));
		viewpager.setOffscreenPageLimit(MainPagerAdapter.NUM_ITEMS);
		updatePage(viewpager.getCurrentItem());

		// Setup tab layout
		TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
		tabLayout.setupWithViewPager(viewpager);
		viewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
			@Override
			public void onPageScrolled(int i, float v, int i1) {
			}

			@Override
			public void onPageSelected(int i) {
				updatePage(i);
			}

			@Override
			public void onPageScrollStateChanged(int i) {
			}
		});
	}

	/**
	 * Sets up the Floating action button.
	 */
	private void setupFab() {

		Fab fab = (Fab) findViewById(R.id.fab);
		View sheetView = findViewById(R.id.fab_sheet);
		View overlay = findViewById(R.id.overlay);
		int sheetColor = getResources().getColor(R.color.background_card);
		int fabColor = getResources().getColor(R.color.theme_accent);

		// Create material sheet FAB
		materialSheetFab = new MaterialSheetFab<>(fab, sheetView, overlay, sheetColor, fabColor);

		// Set material sheet event listener
		materialSheetFab.setEventListener(new MaterialSheetFabEventListener() {
			@Override
			public void onShowSheet() {
				// Save current status bar color
				statusBarColor = getStatusBarColor();
				// Set darker status bar color to match the dim overlay
				setStatusBarColor(getResources().getColor(R.color.theme_primary_dark2));
			}

			@Override
			public void onHideSheet() {
				// Restore status bar color
				setStatusBarColor(statusBarColor);
			}
		});

		// Set material sheet item click listeners
		findViewById(R.id.fab_sheet_item_recording).setOnClickListener(this);
		findViewById(R.id.fab_sheet_item_reminder).setOnClickListener(this);
		findViewById(R.id.fab_sheet_item_photo).setOnClickListener(this);
		findViewById(R.id.fab_sheet_item_note).setOnClickListener(this);
	}

	/**
	 * Called when the selected page changes.
	 *
	 * @param selectedPage selected page
	 */
	private void updatePage(int selectedPage) {
		updateFab(selectedPage);
		updateSnackbar(selectedPage);
	}

	/**
	 * Updates the FAB based on the selected page
	 *
	 * @param selectedPage selected page
	 */
	private void updateFab(int selectedPage) {
		switch (selectedPage) {
		case MainPagerAdapter.ALL_POS:
			materialSheetFab.showFab();
			break;
		case MainPagerAdapter.SHARED_POS:
			materialSheetFab.showFab(0,
					-getResources().getDimensionPixelSize(R.dimen.snackbar_height));
			break;
		case MainPagerAdapter.FAVORITES_POS:
		default:
			materialSheetFab.hideSheetThenFab();
			break;
		}
	}

	/**
	 * Updates the snackbar based on the selected page
	 *
	 * @param selectedPage selected page
	 */
	private void updateSnackbar(int selectedPage) {
		View snackbar = findViewById(R.id.snackbar);
		switch (selectedPage) {
		case MainPagerAdapter.SHARED_POS:
			snackbar.setVisibility(View.VISIBLE);
			break;
		case MainPagerAdapter.ALL_POS:
		case MainPagerAdapter.FAVORITES_POS:
		default:
			snackbar.setVisibility(View.GONE);
			break;
		}
	}

	/**
	 * Toggles opening/closing the drawer.
	 */
	private void toggleDrawer() {
		if (drawerLayout.isDrawerVisible(GravityCompat.START)) {
			drawerLayout.closeDrawer(GravityCompat.START);
		} else {
			drawerLayout.openDrawer(GravityCompat.START);
		}
	}

	@Override
	public void onClick(View v) {
		Toast.makeText(this, R.string.sheet_item_pressed, Toast.LENGTH_SHORT).show();
		materialSheetFab.hideSheet();
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.menu_main, menu);
		return super.onCreateOptionsMenu(menu);
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case android.R.id.home:
			toggleDrawer();
			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}

	private int getStatusBarColor() {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
			return getWindow().getStatusBarColor();
		}
		return 0;
	}

	private void setStatusBarColor(int color) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
			getWindow().setStatusBarColor(color);
		}
	}
}


package io.codetail.animation;

import android.view.animation.Interpolator;

import java.lang.ref.WeakReference;

public abstract class SupportAnimator {

    WeakReference<RevealAnimator> mTarget;

    public SupportAnimator(RevealAnimator target) {
        mTarget = new WeakReference<>(target);
    }

    /**
     * @return true if using native android animation framework, otherwise is
     * nineoldandroids
     */
    public abstract boolean isNativeAnimator();

    /**
     * @return depends from {@link android.os.Build.VERSION} if sdk version
     * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and greater will return
     * {@link android.animation.Animator} otherwise {@link com.nineoldandroids.animation.Animator}
     */
    public abstract Object get();

    /**
     * Starts this animation. If the animation has a nonzero startDelay, the animation will start
     * running after that delay elapses. A non-delayed animation will have its initial
     * value(s) set immediately, followed by calls to
     * {@link android.animation.Animator.AnimatorListener#onAnimationStart(android.animation.Animator)}
     * for any listeners of this animator.
     *
     *
The animation started by calling this method will be run on the thread that called
     * this method. This thread should have a Looper on it (a runtime exception will be thrown if
     * this is not the case). Also, if the animation will animate
     * properties of objects in the view hierarchy, then the calling thread should be the UI
     * thread for that view hierarchy.

     *
     */
    public abstract void start();

    /**
     * Sets the duration of the animation.
     *
     * @param duration The length of the animation, in milliseconds.
     */
    public abstract void setDuration(int duration);

    /**
     * The time interpolator used in calculating the elapsed fraction of the
     * animation. The interpolator determines whether the animation runs with
     * linear or non-linear motion, such as acceleration and deceleration. The
     * default value is {@link android.view.animation.AccelerateDecelerateInterpolator}.
     *
     * @param value the interpolator to be used by this animation
     */
    public abstract void setInterpolator(Interpolator value);

    /**
     * Adds a listener to the set of listeners that are sent events through the life of an
     * animation, such as start, repeat, and end.
     *
     * @param listener the listener to be added to the current set of listeners for this animation.
     */
    public abstract void addListener(AnimatorListener listener);

    /**
     * Returns whether this Animator is currently running (having been started and gone past any
     * initial startDelay period and not yet ended).
     *
     * @return Whether the Animator is running.
     */
    public abstract boolean isRunning();

    /**
     * Cancels the animation. Unlike {@link #end()}, cancel() causes the animation to
     * stop in its tracks, sending an
     * {@link AnimatorListener#onAnimationCancel()} to
     * its listeners, followed by an
     * {@link AnimatorListener#onAnimationEnd()} message.
     *
     *
This method must be called on the thread that is running the animation.

     */
    public abstract void cancel();

    /**
     * Ends the animation. This causes the animation to assign the end value of the property being
     * animated, then calling the
     * {@link AnimatorListener#onAnimationEnd()} method on
     * its listeners.
     *
     *
This method must be called on the thread that is running the animation.

     */
    public void end() {
    }

    /**
     * This method tells the object to use appropriate information to extract
     * starting values for the animation. For example, a AnimatorSet object will pass
     * this call to its child objects to tell them to set up the values. A
     * ObjectAnimator object will use the information it has about its target object
     * and PropertyValuesHolder objects to get the start values for its properties.
     * A ValueAnimator object will ignore the request since it does not have enough
     * information (such as a target object) to gather these values.
     */
    public void setupStartValues() {
    }

    /**
     * This method tells the object to use appropriate information to extract
     * ending values for the animation. For example, a AnimatorSet object will pass
     * this call to its child objects to tell them to set up the values. A
     * ObjectAnimator object will use the information it has about its target object
     * and PropertyValuesHolder objects to get the start values for its properties.
     * A ValueAnimator object will ignore the request since it does not have enough
     * information (such as a target object) to gather these values.
     */
    public void setupEndValues() {
    }

    /**
     * Experimental feature
     */
    public SupportAnimator reverse() {
        if(isRunning()){
            return null;
        }

        RevealAnimator target = mTarget.get();
        if(target != null){
            return target.startReverseAnimation();
        }

        return null;
    }

    /**
     *
An animation listener receives notifications from an animation.
     * Notifications indicate animation related events, such as the end or the
     * repetition of the animation.

     */
    public interface AnimatorListener {
        /**
         *
Notifies the start of the animation.

         */
        void onAnimationStart();

        /**
         *
Notifies the end of the animation. This callback is not invoked
         * for animations with repeat count set to INFINITE.

         */
        void onAnimationEnd();

        /**
         *
Notifies the cancellation of the animation. This callback is not invoked
         * for animations with repeat count set to INFINITE.

         */
        void onAnimationCancel();

        /**
         *
Notifies the repetition of the animation.

         */
        void onAnimationRepeat();
    }

    /**
     *
Provides default implementation for AnimatorListener.

     */
    public static abstract class SimpleAnimatorListener implements AnimatorListener {

        @Override
        public void onAnimationStart() {

        }

        @Override
        public void onAnimationEnd() {

        }

        @Override
        public void onAnimationCancel() {

        }

        @Override
        public void onAnimationRepeat() {

        }
    }

}


package com.gordonwong.materialsheetfab.animations;

import android.view.View;
import android.view.animation.Interpolator;

import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;

import io.codetail.animation.arcanimator.ArcAnimator;
import io.codetail.animation.arcanimator.Side;

/**
 * Created by Gordon Wong on 7/9/2015.
 *
 * Animates the FAB when showing and hiding the material sheet.
 */
public class FabAnimation {

	protected View fab;
	protected Interpolator interpolator;

	public FabAnimation(View fab, Interpolator interpolator) {
		this.fab = fab;
		this.interpolator = interpolator;
	}

	/**
	 * Animates the FAB as if the FAB is morphing into a sheet.
	 *
	 * @param endX The X coordinate that the FAB will be moved to.
	 * @param endY The Y coordinate that the FAB will be moved to.
	 * @param side The side of the arc animation.
	 * @param arcDegrees Amount of arc in FAB movement animation.
	 * @param scaleFactor Amount to scale FAB.
	 * @param duration Duration of the animation in milliseconds. Use 0 for no animation.
	 * @param listener Listener for animation events.
	 */
	public void morphIntoSheet(int endX, int endY, Side side, int arcDegrees, float scaleFactor,
			long duration, AnimationListener listener) {
		morph(endX, endY, side, arcDegrees, scaleFactor, duration, listener);
	}

	/**
	 * Animates the FAB as if a sheet is being morphed into a FAB.
	 *
	 * @param endX The X coordinate that the FAB will be moved to.
	 * @param endY The Y coordinate that the FAB will be moved to.
	 * @param side The side of the arc animation.
	 * @param arcDegrees Amount of arc in FAB movement animation.
	 * @param scaleFactor Amount to scale FAB.
	 * @param duration Duration of the animation in milliseconds. Use 0 for no animation.
	 * @param listener Listener for animation events.
	 */
	public void morphFromSheet(int endX, int endY, Side side, int arcDegrees, float scaleFactor,
			long duration, AnimationListener listener) {
		fab.setVisibility(View.VISIBLE);
		morph(endX, endY, side, arcDegrees, scaleFactor, duration, listener);
	}

	protected void morph(float endX, float endY, Side side, float arcDegrees, float scaleFactor,
			long duration, AnimationListener listener) {
		// Move the FAB
		startArcAnim(fab, endX, endY, arcDegrees, side, duration, interpolator, listener);

		// Scale the size of the FAB
		fab.animate().scaleXBy(scaleFactor).scaleYBy(scaleFactor).setDuration(duration)
				.setInterpolator(interpolator).start();
	}

	protected void startArcAnim(View view, float endX, float endY, float degrees, Side side,
			long duration, Interpolator interpolator, final AnimationListener listener) {
		// Setup animation
		// Cast end coordinates to ints so that the FAB will be animated to the same position even
		// when there are minute differences in the coordinates
		ArcAnimator anim = ArcAnimator.createArcAnimator(view, (int) endX, (int) endY, degrees,
				side);
		anim.setDuration(duration);
		anim.setInterpolator(interpolator);
		// Add listener
		anim.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationStart(Animator animation) {
				if (listener != null) {
					listener.onStart();
				}
			}

			@Override
			public void onAnimationEnd(Animator animation) {
				if (listener != null) {
					listener.onEnd();
				}
			}
		});
		// Start animation
		anim.start();
	}

}


/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/

package com.linkedin.android.spyglass.sample.data.models;

import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.NonNull;

import com.linkedin.android.spyglass.mentions.Mentionable;
import com.linkedin.android.spyglass.sample.R;
import com.linkedin.android.spyglass.sample.data.MentionsLoader;
import com.linkedin.android.spyglass.tokenization.QueryToken;

import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

/**
 * Model representing a person.
 */
public class Person implements Mentionable {

    private final String mFirstName;
    private final String mLastName;
    private final String mPictureURL;

    public Person(String firstName, String lastName, String pictureURL) {
        mFirstName = firstName;
        mLastName = lastName;
        mPictureURL = pictureURL;
    }

    public String getFirstName() {
        return mFirstName;
    }

    public String getLastName() {
        return mLastName;
    }

    public String getFullName() {
        return getFirstName() + " " + getLastName();
    }

    public String getPictureURL() {
        return mPictureURL;
    }

    // --------------------------------------------------
    // Mentionable/Suggestible Implementation
    // --------------------------------------------------

    @NonNull
    @Override
    public String getTextForDisplayMode(MentionDisplayMode mode) {
        switch (mode) {
            case FULL:
                return getFullName();
            case PARTIAL:
                String[] words = getFullName().split(" ");
                return (words.length > 1) ? words[0] : "";
            case NONE:
            default:
                return "";
        }
    }

    @Override
    public MentionDeleteStyle getDeleteStyle() {
        // People support partial deletion
        // i.e. "John Doe" -> DEL -> "John" -> DEL -> ""
        return MentionDeleteStyle.PARTIAL_NAME_DELETE;
    }

    @Override
    public int getSuggestibleId() {
        return getFullName().hashCode();
    }

    @Override
    public String getSuggestiblePrimaryText() {
        return getFullName();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mFirstName);
        dest.writeString(mLastName);
        dest.writeString(mPictureURL);
    }

    public Person(Parcel in) {
        mFirstName = in.readString();
        mLastName = in.readString();
        mPictureURL = in.readString();
    }

    public static final Parcelable.Creator<Person> CREATOR
            = new Parcelable.Creator<Person>() {
        public Person createFromParcel(Parcel in) {
            return new Person(in);
        }

        public Person[] newArray(int size) {
            return new Person[size];
        }
    };

    // --------------------------------------------------
    // PersonLoader Class (loads people from JSON file)
    // --------------------------------------------------

    public static class PersonLoader extends MentionsLoader<Person> {
        private static final String TAG = PersonLoader.class.getSimpleName();

        public PersonLoader(Resources res) {
            super(res, R.raw.people);
        }

        @Override
        public Person[] loadData(JSONArray arr) {
            Person[] data = new Person[arr.length()];
            try {
                for (int i = 0; i < arr.length(); i++) {
                    JSONObject obj = arr.getJSONObject(i);
                    String first = obj.getString("first");
                    String last = obj.getString("last");
                    String url = obj.getString("picture");
                    data[i] = new Person(first, last, url);
                }
            } catch (Exception e) {
                Log.e(TAG, "Unhandled exception while parsing person JSONArray", e);
            }

            return data;
        }

        // Modified to return suggestions based on both first and last name
        @Override
        public List<Person> getSuggestions(QueryToken queryToken) {
            String[] namePrefixes = queryToken.getKeywords().toLowerCase().split(" ");
            List<Person> suggestions = new ArrayList<>();
            if (mData != null) {
                for (Person suggestion : mData) {
                    String firstName = suggestion.getFirstName().toLowerCase();
                    String lastName = suggestion.getLastName().toLowerCase();
                    if (namePrefixes.length == 2) {
                        if (firstName.startsWith(namePrefixes[0]) && lastName.startsWith(namePrefixes[1])) {
                            suggestions.add(suggestion);
                        }
                    } else {
                        if (firstName.startsWith(namePrefixes[0]) || lastName.startsWith(namePrefixes[0])) {
                            suggestions.add(suggestion);
                        }
                    }
                }
            }
            return suggestions;
        }
    }
}


/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/

package com.linkedin.android.spyglass.suggestions;

import android.content.Context;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import androidx.annotation.NonNull;

import com.linkedin.android.spyglass.suggestions.interfaces.Suggestible;
import com.linkedin.android.spyglass.suggestions.interfaces.SuggestionsListBuilder;
import com.linkedin.android.spyglass.suggestions.interfaces.SuggestionsVisibilityManager;
import com.linkedin.android.spyglass.tokenization.QueryToken;
import com.linkedin.android.spyglass.tokenization.interfaces.TokenSource;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Adapter class for displaying suggestions.
 */
public class SuggestionsAdapter extends BaseAdapter {

    private final Object mLock = new Object();
    private final Context mContext;
    private final Resources mResources;
    private final LayoutInflater mInflater;

    private SuggestionsVisibilityManager mSuggestionsVisibilityManager;
    private SuggestionsListBuilder mSuggestionsListBuilder;
    private final List<Suggestible> mSuggestions;

    // Map from a given bucket (defined by a unique string) to the latest query result for that bucket
    // Example buckets: "Person-Database", "Person-Network", "Companies-Database", "Companies-Network"
    private final Map<String, SuggestionsResult> mResultMap = new HashMap<>();
    private final Map<QueryToken, Set<String>> mWaitingForResults = new HashMap<>();

    public SuggestionsAdapter(final @NonNull Context context,
                              final @NonNull SuggestionsVisibilityManager suggestionsVisibilityManager,
                              final @NonNull SuggestionsListBuilder suggestionsListBuilder) {
        super();
        mContext = context;
        mResources = context.getResources();
        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mSuggestionsVisibilityManager = suggestionsVisibilityManager;
        mSuggestionsListBuilder = suggestionsListBuilder;
        mSuggestions = new ArrayList<>();
    }

    // --------------------------------------------------
    // Public Methods
    // --------------------------------------------------

    /**
     * Method to notify the adapter that a new {@link QueryToken} has been received and that
     * suggestions will be added to the adapter once generated.
     *
     * @param queryToken the {@link QueryToken} that has been received
     * @param buckets    a list of string dictating which buckets the future query results will go into
     */

    public void notifyQueryTokenReceived(QueryToken queryToken, List<String> buckets) {
        synchronized (mLock) {
            Set<String> currentBuckets = mWaitingForResults.get(queryToken);
            if (currentBuckets == null) {
                currentBuckets = new HashSet<>();
            }
            currentBuckets.addAll(buckets);
            mWaitingForResults.put(queryToken, currentBuckets);
        }
    }

    /**
     * Add mention suggestions to a given bucket in the adapter. The adapter tracks the latest result for every given
     * bucket, and passes this information to the SuggestionsManager to construct the list of suggestions in the
     * appropriate order.
     * <p>
     * Note: This should be called exactly once for every bucket returned from the query client.
     *
     * @param result a {@link SuggestionsResult} containing the suggestions to add
     * @param bucket a string representing the group to place the {@link SuggestionsResult} into
     * @param source the associated {@link TokenSource} to use for reference
     */
    public void addSuggestions(final @NonNull SuggestionsResult result,
                               final @NonNull String bucket,
                               final @NonNull TokenSource source) {
        // Add result to proper bucket and remove from waiting
        QueryToken query = result.getQueryToken();
        synchronized (mLock) {
            mResultMap.put(bucket, result);
            Set<String> waitingForBuckets = mWaitingForResults.get(query);
            if (waitingForBuckets != null) {
                waitingForBuckets.remove(bucket);
                if (waitingForBuckets.size() == 0) {
                    mWaitingForResults.remove(query);
                }
            }
        }

        // Rebuild the list of suggestions in the appropriate order
        String currentTokenString = source.getCurrentTokenString();
        synchronized (mLock) {
            mSuggestions.clear();
            List<Suggestible> suggestions = mSuggestionsListBuilder.buildSuggestions(mResultMap, currentTokenString);

            // If we have suggestions, add them to the adapter and display them
            if (suggestions.size() > 0) {
                mSuggestions.addAll(suggestions);
                mSuggestionsVisibilityManager.displaySuggestions(true);
            } else {
                hideSuggestionsIfNecessary(result.getQueryToken(), source);
            }
        }

        notifyDataSetChanged();
    }

    /**
     * Clear all data from adapter.
     */
    public void clear() {
        mResultMap.clear();
        notifyDataSetChanged();
    }

    // --------------------------------------------------
    // Private Helper Methods
    // --------------------------------------------------

    /**
     * Hides the suggestions if there are no more incoming queries.
     *
     * @param currentQuery the most recent {@link QueryToken}
     * @param source       the associated {@link TokenSource} to use for reference
     */
    private void hideSuggestionsIfNecessary(final @NonNull QueryToken currentQuery,
                                            final @NonNull TokenSource source) {
        String queryTS = currentQuery.getTokenString();
        String currentTS = source.getCurrentTokenString();
        if (!isWaitingForResults(currentQuery) && queryTS != null && queryTS.equals(currentTS)) {
            mSuggestionsVisibilityManager.displaySuggestions(false);
        }
    }

    /**
     * Determines if the adapter is still waiting for results for a given {@link QueryToken}
     *
     * @param currentQuery the {@link QueryToken} to check if waiting for results on
     *
     * @return true if still waiting for the results of the current query
     */
    private boolean isWaitingForResults(QueryToken currentQuery) {
        synchronized (mLock) {
            Set<String> buckets = mWaitingForResults.get(currentQuery);
            return buckets != null && buckets.size() > 0;
        }
    }

    // --------------------------------------------------
    // BaseAdapter Overrides
    // --------------------------------------------------

    @Override
    public int getCount() {
        return mSuggestions.size();
    }

    @Override
    public Suggestible getItem(int position) {
        Suggestible mention = null;
        if (position >= 0 && position < mSuggestions.size()) {
            mention = mSuggestions.get(position);
        }
        return mention;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final Suggestible suggestion = getItem(position);
        View v = null;
        if (mSuggestionsVisibilityManager != null) {
            v = mSuggestionsListBuilder.getView(suggestion, convertView, parent, mContext, mInflater, mResources);
        }
        return v;
    }

    // --------------------------------------------------
    // Setters
    // --------------------------------------------------

    /**
     * Sets the {@link com.linkedin.android.spyglass.suggestions.interfaces.SuggestionsVisibilityManager} to use.
     *
     * @param suggestionsVisibilityManager the {@link com.linkedin.android.spyglass.suggestions.interfaces.SuggestionsVisibilityManager} to use
     */
    public void setSuggestionsManager(final @NonNull SuggestionsVisibilityManager suggestionsVisibilityManager) {
        mSuggestionsVisibilityManager = suggestionsVisibilityManager;
    }

    /**
     * Sets the {@link SuggestionsListBuilder} to use.
     *
     * @param suggestionsListBuilder the {@link SuggestionsListBuilder} to use
     */
    public void setSuggestionsListBuilder(final @NonNull SuggestionsListBuilder suggestionsListBuilder) {
        mSuggestionsListBuilder = suggestionsListBuilder;
    }

}


/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/

package com.linkedin.android.spyglass.sample.data;

import android.content.res.Resources;
import android.os.AsyncTask;
import android.util.Log;

import com.linkedin.android.spyglass.mentions.Mentionable;
import com.linkedin.android.spyglass.tokenization.QueryToken;

import org.json.JSONArray;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * Simple class to get suggestions from a JSONArray (represented as a file on disk), which can then
 * be mentioned by the user by tapping on the suggestion.
 */
public  abstract class MentionsLoader<T extends Mentionable> {

    protected T[] mData;
    private static final String TAG = MentionsLoader.class.getSimpleName();

    public MentionsLoader(final Resources res, final int resID) {
        new LoadJSONArray(res, resID).execute();
    }

    public abstract T[] loadData(JSONArray arr);

    // Returns a subset
    public List<T> getSuggestions(QueryToken queryToken) {
        String prefix = queryToken.getKeywords().toLowerCase();
        List<T> suggestions = new ArrayList<>();
        if (mData != null) {
            for (T suggestion : mData) {
                String name = suggestion.getSuggestiblePrimaryText().toLowerCase();
                if (name.startsWith(prefix)) {
                    suggestions.add(suggestion);
                }
            }
        }
        return suggestions;
    }

    // Loads data from JSONArray file, defined in the raw resources folder
    private class LoadJSONArray extends AsyncTask<Void, Void, JSONArray> {

        private final WeakReference<Resources> mRes;
        private final int mResId;

        public LoadJSONArray(Resources res, int resId) {
            mRes = new WeakReference<>(res);
            mResId = resId;
        }

        @Override
        protected JSONArray doInBackground(Void... params) {
            InputStream fileReader = mRes.get().openRawResource(mResId);
            Writer writer = new StringWriter();
            JSONArray arr = null;
            try {
                BufferedReader reader = new BufferedReader(new InputStreamReader(fileReader, "UTF-8"));
                String line = reader.readLine();
                while (line != null) {
                    writer.write(line);
                    line = reader.readLine();
                }
                String jsonString = writer.toString();
                arr = new JSONArray(jsonString);
            } catch (Exception e) {
                Log.e(TAG, "Unhandled exception while reading JSON", e);
            } finally {
                try {
                    fileReader.close();
                } catch (Exception e) {
                    Log.e(TAG, "Unhandled exception while closing JSON file", e);
                }
            }
            return arr;
        }

        @Override
        protected void onPostExecute(JSONArray arr) {
            super.onPostExecute(arr);
            mData = loadData(arr);
        }
    }
}


/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/

package com.linkedin.android.spyglass.tokenization.impl;

import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.EditText;
import androidx.annotation.NonNull;
import com.linkedin.android.spyglass.mentions.MentionSpan;
import com.linkedin.android.spyglass.tokenization.interfaces.Tokenizer;

/**
 * Tokenizer class used to determine the keywords to be used when querying for mention suggestions.
 */
public class WordTokenizer implements Tokenizer {

    private final WordTokenizerConfig mConfig;

    public WordTokenizer() {
        this(new WordTokenizerConfig.Builder().build());
    }

    public WordTokenizer(final @NonNull WordTokenizerConfig config) {
        mConfig = config;
    }

    // --------------------------------------------------
    // Tokenizer Interface Implementation
    // --------------------------------------------------

    /**
     * {@inheritDoc}
     */
    @Override
    public int findTokenStart(final @NonNull Spanned text, final int cursor) {
        int start = getSearchStartIndex(text, cursor);
        int i = cursor;

        // If it is explicit, return the index of the first explicit character
        if (isExplicit(text, cursor)) {
            i--;
            while (i >= start) {
                char currentChar = text.charAt(i);
                if (isExplicitChar(currentChar)) {
                    if (i == 0 || isWordBreakingChar(text.charAt(i - 1))) {
                        return i;
                    }
                }
                i--;
            }
            // Could not find explicit character before the cursor
            // Note: This case should never happen (means that isExplicit
            // returned true when it should have been false)
            return -1;

        } else {

            // For implicit tokens, we need to go back a certain number of words to find the start
            // of the token (with the max number of words to go back defined in the config)
            int maxNumKeywords = mConfig.MAX_NUM_KEYWORDS;

            // Go back to the start of the word that the cursor is currently in
            while (i > start && !isWordBreakingChar(text.charAt(i - 1))) {
                i--;
            }

            // Cursor is at beginning of current word, go back MaxNumKeywords - 1 now
            for (int j = 0; j < maxNumKeywords - 1; j++) {
                // Decrement through only one word-breaking character, if it exists
                if (i > start && isWordBreakingChar(text.charAt(i - 1))) {
                    i--;
                }
                // If there is more than one word-breaking space, break out now
                // Do not consider queries with words separated by more than one word-breaking char
                if (i > start && isWordBreakingChar(text.charAt(i - 1))) {
                    break;
                }
                // Decrement until the next space
                while (i > start && !isWordBreakingChar(text.charAt(i - 1))) {
                    i--;
                }
            }

            // Ensures that text.char(i) is not a word-breaking or explicit char (i.e. cursor must have a
            // word-breaking char in front of it and a non-word-breaking char behind it)
            while (i < cursor && (isWordBreakingChar(text.charAt(i)) || isExplicitChar(text.charAt(i)))) {
                i++;
            }

            return i;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int findTokenEnd(final @NonNull Spanned text, final int cursor) {
        int i = cursor;
        int end = getSearchEndIndex(text, cursor);

        // Starting from the cursor, increment i until it reaches the first word-breaking char
        while (i >= 0 && i < end) {
            if (isWordBreakingChar(text.charAt(i))) {
                return i;
            } else {
                i++;
            }
        }

        return i;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isValidMention(final @NonNull Spanned text, final int start, final int end) {
        // Get the token
        CharSequence token = text.subSequence(start, end);

        // Null or empty string is not a valid mention
        if (TextUtils.isEmpty(token)) {
            return false;
        }

        // Handle explicit mentions first, then implicit mentions
        final int threshold = mConfig.THRESHOLD;
        boolean multipleWords = containsWordBreakingChar(token);
        boolean containsExplicitChar = containsExplicitChar(token);

        if (!multipleWords && containsExplicitChar) {

            // If it is one word and has an explicit char, the explicit char must be the first char
            if (!isExplicitChar(token.charAt(0))) {
                return false;
            }

            // Ensure that the character before the explicit character is a word-breaking character
            // Note: Checks the explicit character closest in front of the cursor
            if (!hasWordBreakingCharBeforeExplicitChar(text, end)) {
                return false;
            }

            // Return true if string is just an explicit character
            if (token.length() == 1) {
                return true;
            }

            // If input has length greater than one, the second character must be a letter or digit
            // Return true if and only if second character is a letter or digit, i.e. "@d"
            return Character.isLetterOrDigit(token.charAt(1));

        } else if (token.length() >= threshold) {

            // Change behavior depending on if keywords is one or more words
            if (!multipleWords) {
                // One word, no explicit characters
                // input is only one word, i.e. "u41"
                return onlyLettersOrDigits(token, threshold, 0);
            } else if (containsExplicitChar) {
                // Multiple words, has explicit character
                // Must have a space, the explicit character, then a letter or digit
                return hasWordBreakingCharBeforeExplicitChar(text, end)
                        && isExplicitChar(token.charAt(0))
                        && Character.isLetterOrDigit(token.charAt(1));
            } else {
                // Multiple words, no explicit character
                // Either the first or last couple of characters must be letters/digits
                boolean firstCharactersValid = onlyLettersOrDigits(token, threshold, 0);
                boolean lastCharactersValid = onlyLettersOrDigits(token, threshold, token.length() - threshold);
                return firstCharactersValid || lastCharactersValid;
            }
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @NonNull
    public Spanned terminateToken(final @NonNull Spanned text) {
        // Note: We do not need to modify the text to terminate it
        return text;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isExplicitChar(final char c) {
        final String explicitChars = mConfig.EXPLICIT_CHARS;
        for (int i = 0; i < explicitChars.length(); i++) {
            char explicitChar = explicitChars.charAt(i);
            if (c == explicitChar) {
                return true;
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isWordBreakingChar(final char c) {
        final String wordBreakChars = mConfig.WORD_BREAK_CHARS;
        for (int i = 0; i < wordBreakChars.length(); i++) {
            char wordBreakChar = wordBreakChars.charAt(i);
            if (c == wordBreakChar) {
                return true;
            }
        }
        return false;
    }

    // --------------------------------------------------
    // Public Methods
    // --------------------------------------------------

    /**
     * Returns true if and only if there is an explicit character before the cursor
     * but after any previous mentions. There must be a word-breaking character before the
     * explicit character.
     *
     * @param text   String to determine if it is explicit or not
     * @param cursor position of the cursor in text
     *
     * @return true if the current keywords are explicit (i.e. explicit character typed before cursor)
     */
    public boolean isExplicit(final @NonNull CharSequence text, final int cursor) {
        return getExplicitChar(text, cursor) != (char) 0;
    }

    /**
     * Returns the explicit character if appropriate (i.e. within the keywords).
     * If not currently explicit, then returns the null character (i.e. '/0').
     *
     * @param text   String to get the explicit character from
     * @param cursor position of the cursor in text
     *
     * @return the current explicit character or the null character if not currently explicit
     */
    public char getExplicitChar(final @NonNull CharSequence text, final int cursor) {
        if (cursor < 0 || cursor > text.length()) {
            return (char) 0;
        }
        SpannableStringBuilder ssb = new SpannableStringBuilder(text);
        int start = getSearchStartIndex(ssb, cursor);
        int i = cursor - 1;
        int numWordBreakingCharsSeen = 0;
        while (i >= start) {
            char currentChar = text.charAt(i);
            if (isExplicitChar(currentChar)) {
                // Explicit character must have a word-breaking character before it
                if (i == 0 || isWordBreakingChar(text.charAt(i - 1))) {
                    return currentChar;
                } else {
                    // Otherwise, explicit character is not in a valid position, return null char
                    return (char) 0;
                }
            } else if (isWordBreakingChar(currentChar)) {
                // Do not allow the explicit mention to exceed
                numWordBreakingCharsSeen++;
                if (numWordBreakingCharsSeen == mConfig.MAX_NUM_KEYWORDS) {
                    // No explicit char in maxNumKeywords, so return null char
                    return (char) 0;
                }
            }
            i--;
        }
        return (char) 0;
    }

    /**
     * Returns true if the input string contains an explicit character.
     *
     * @param input a {@link CharSequence} to test
     *
     * @return true if input contains an explicit character
     */
    public boolean containsExplicitChar(final @NonNull CharSequence input) {
        if (!TextUtils.isEmpty(input)) {
            for (int i = 0; i < input.length(); i++) {
                char c = input.charAt(i);
                if (isExplicitChar(c)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns true if the input string contains a word-breaking character.
     *
     * @param input a {@link CharSequence} to test
     *
     * @return true if input contains a word-breaking character
     */
    public boolean containsWordBreakingChar(final @NonNull CharSequence input) {
        if (!TextUtils.isEmpty(input)) {
            for (int i = 0; i < input.length(); i++) {
                char c = input.charAt(i);
                if (isWordBreakingChar(c)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Given a string and starting index, return true if the first "numCharsToCheck" characters at
     * the starting index are either a letter or a digit.
     *
     * @param input           a {@link CharSequence} to test
     * @param numCharsToCheck number of characters to examine at starting position
     * @param start           starting position within the input string
     *
     * @return true if the first "numCharsToCheck" at the starting index are either letters or digits
     */
    public boolean onlyLettersOrDigits(final @NonNull CharSequence input, final int numCharsToCheck, final int start) {

        // Starting position must be within the input string
        if (start < 0 || start > input.length()) {
            return false;
        }

        // Check the first "numCharsToCheck" characters to ensure they are a letter or digit
        for (int i = 0; i < numCharsToCheck; i++) {
            int positionToCheck = start + i;
            // Return false if we would throw an Out-of-Bounds exception
            if (positionToCheck >= input.length()) {
                return false;
            }
            // Return false early if current character is not a letter or digit
            char charToCheck = input.charAt(positionToCheck);
            if (!Character.isLetterOrDigit(charToCheck)) {
                return false;
            }
        }

        // First "numCharsToCheck" characters are either letters or digits, so return true
        return true;
    }

    // --------------------------------------------------
    // Protected Helper Methods
    // --------------------------------------------------

    /**
     * Returns the index of the end of the last span before the cursor or
     * the start of the current line if there are no spans before the cursor.
     *
     * @param text   the {@link Spanned} to examine
     * @param cursor position of the cursor in text
     *
     * @return the furthest in front of the cursor to search for the current keywords
     */
    protected int getSearchStartIndex(final @NonNull Spanned text, int cursor) {
        if (cursor < 0 || cursor > text.length()) {
            cursor = 0;
        }

        // Get index of the end of the last span before the cursor (or 0 if does not exist)
        MentionSpan[] spans = text.getSpans(0, text.length(), MentionSpan.class);
        int closestToCursor = 0;
        for (MentionSpan span : spans) {
            int end = text.getSpanEnd(span);
            if (end > closestToCursor && end <= cursor) {
                closestToCursor = end;
            }
        }

        // Get the index of the start of the line
        String textString = text.toString().substring(0, cursor);
        int lineStartIndex = 0;
        if (textString.contains(mConfig.LINE_SEPARATOR)) {
            lineStartIndex = textString.lastIndexOf(mConfig.LINE_SEPARATOR) + 1;
        }

        // Return whichever is closer before to the cursor
        return Math.max(closestToCursor, lineStartIndex);
    }

    /**
     * Returns the index of the beginning of the first span after the cursor or
     * length of the text if there are no spans after the cursor.
     *
     * @param text   the {@link Spanned} to examine
     * @param cursor position of the cursor in text
     *
     * @return the furthest behind the cursor to search for the current keywords
     */
    protected int getSearchEndIndex(final @NonNull Spanned text, int cursor) {
        if (cursor < 0 || cursor > text.length()) {
            cursor = 0;
        }

        // Get index of the start of the first span after the cursor (or text.length() if does not exist)
        MentionSpan[] spans = text.getSpans(0, text.length(), MentionSpan.class);
        int closestAfterCursor = text.length();
        for (MentionSpan span : spans) {
            int start = text.getSpanStart(span);
            if (start < closestAfterCursor && start >= cursor) {
                closestAfterCursor = start;
            }
        }

        // Get the index of the end of the line
        String textString = text.toString().substring(cursor, text.length());
        int lineEndIndex = text.length();
        if (textString.contains(mConfig.LINE_SEPARATOR)) {
            lineEndIndex = cursor + textString.indexOf(mConfig.LINE_SEPARATOR);
        }

        // Return whichever is closest after the cursor
        return Math.min(closestAfterCursor, lineEndIndex);
    }

    /**
     * Ensure the the character before the explicit character is a word-breaking character.
     * Note that the function only uses the input string to determine which explicit character was
     * typed. It uses the complete contents of the {@link EditText} to determine if there is a
     * word-breaking character before the explicit character, as the input string may start with an
     * explicit character, i.e. with an input string of "@John Doe" and an {@link EditText} containing
     * the string "Hello @John Doe", this should return true.
     *
     * @param text   the {@link Spanned} to check for a word-breaking character before the explicit character
     * @param cursor position of the cursor in text
     *
     * @return true if there is a space before the explicit character, false otherwise
     */
    protected boolean hasWordBreakingCharBeforeExplicitChar(final @NonNull Spanned text, final int cursor) {
        CharSequence beforeCursor = text.subSequence(0, cursor);
        // Get the explicit character closest before the cursor and make sure it
        // has a word-breaking character in front of it
        int i = cursor - 1;
        while (i >= 0 && i < beforeCursor.length()) {
            char c = beforeCursor.charAt(i);
            if (isExplicitChar(c)) {
                return i == 0 || isWordBreakingChar(beforeCursor.charAt(i - 1));
            }
            i--;
        }
        return false;
    }

}