app/src/main/java/com/ratebeer/android/gui/lists/RatingsAdapter.java
public final class RatingsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

	private static final int TYPE_HEADER = 0;
	private static final int TYPE_ITEM = 1;

	private final Context context;
	private final MenuInflater menuInflater;
	private List<Rating> ratings;

	public RatingsAdapter(Context context, MenuInflater menuInflater, List<Rating> ratings) {
		this.context = context;
		this.ratings = ratings;
		this.menuInflater = menuInflater;
	}

	@Override
	public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		if (viewType == TYPE_HEADER && Session.get().isLoggedIn())
			return new HeaderHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_header_account, parent, false), menuInflater);
		else
			return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_rating, parent, false));
	}

	@Override
	public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
		if (holder instanceof HeaderHolder) {
			HeaderHolder headerHolder = (HeaderHolder) holder;
			Images.with(headerHolder.avatarImage.getContext()).loadUser(Session.get().getUserName()).placeholder(R.color.grey_dark).fit()
					.centerCrop()
					.into(headerHolder.avatarImage);
			headerHolder.userNameText.setText(Session.get().getUserName());
			headerHolder.refreshMenu.setOnMenuItemClickListener(item -> {
				if (item.getItemId() == R.id.menu_refresh) {
					context.startService(SyncService.start(context));
				}
				return true;
			});
			// Monitor changes in the user counts too
			Session.get().getUpdates(context, true).observeOn(AndroidSchedulers.mainThread()).subscribe(
					counts -> headerHolder.userCountText.setText(String.format(Locale.getDefault(), "%1$d", Session.get().getUserRateCount())));
		} else {
			Rating rating = ratings.get(position - 1);
			ItemHolder itemHolder = (ItemHolder) holder;
			if (rating.beerId != null && rating.beerId > 0) {
				Images.with(itemHolder.photoImage.getContext()).loadBeer(rating.beerId).placeholder(android.R.color.white).fit().centerInside()
						.into(itemHolder.photoImage);
			} else {
				itemHolder.rowLayout.setBackgroundResource(0);
				itemHolder.photoImage.setImageResource(0);
			}
			itemHolder.ratingMarkText.setBackgroundResource(ImageUrls.getColor(position, true));
			itemHolder.beerNameText.setText(rating.beerName);
			if (rating.isUploaded()) {
				// Already uploaded to RB: show rating date
				itemHolder.ratingMarkText.setText(String.format(Locale.getDefault(), "%1$.1f", rating.total));
				itemHolder.offlineBadge.setVisibility(View.GONE);
				itemHolder.timeEnteredText.setVisibility(View.VISIBLE);
				itemHolder.timeEnteredText.setText(DateUtils.formatDateTime(context, rating.timeEntered.getTime(),
						DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_NUMERIC_DATE));
			} else {
				// Offline rating
				itemHolder.ratingMarkText.setText("-");
				itemHolder.offlineBadge.setVisibility(View.VISIBLE);
				itemHolder.timeEnteredText.setVisibility(View.GONE);
			}
			itemHolder.brewerNameText.setText(rating.brewerName);
		}
	}

	@Override
	public int getItemCount() {
		return ratings.size() + 1;
	}

	@Override
	public int getItemViewType(int position) {
		return position == 0 ? TYPE_HEADER : TYPE_ITEM;
	}

	public Rating get(int position) {
		if (position == 0)
			return null; // Header, not a rating
		return ratings.get(position - 1);
	}

	public void update(List<Rating> ratings) {
		this.ratings = ratings;
		notifyDataSetChanged();
	}

	static class HeaderHolder extends RecyclerView.ViewHolder {

		final ImageView avatarImage;
		final TextView userNameText;
		final ActionMenuView refreshMenu;
		final TextView userCountText;

		public HeaderHolder(View v, MenuInflater menuInflater) {
			super(v);
			avatarImage = (ImageView) v.findViewById(R.id.avatar_image);
			userNameText = (TextView) v.findViewById(R.id.user_name_text);
			userCountText = (TextView) v.findViewById(R.id.user_count_text);
			refreshMenu = (ActionMenuView) v.findViewById(R.id.refresh_menu);
			menuInflater.inflate(R.menu.menu_refresh, refreshMenu.getMenu());
		}

	}

	static class ItemHolder extends RecyclerView.ViewHolder {

		final View rowLayout;
		final TextView ratingMarkText;
		final TextView beerNameText;
		final ImageView photoImage;
		final TextView timeEnteredText;
		final TextView brewerNameText;
		final View offlineBadge;

		public ItemHolder(View v) {
			super(v);
			rowLayout = v;
			ratingMarkText = (TextView) v.findViewById(R.id.rating_mark_text);
			beerNameText = (TextView) v.findViewById(R.id.beer_name_text);
			photoImage = (ImageView) v.findViewById(R.id.photo_image);
			timeEnteredText = (TextView) v.findViewById(R.id.date_text);
			brewerNameText = (TextView) v.findViewById(R.id.brewer_name_text);
			offlineBadge = v.findViewById(R.id.offline_badge);
		}

	}

}
app/src/main/java/com/ratebeer/android/db/Db.java
public final class Db {

	private static final long MAX_AGE = 30 * 60 * 1000; // 30 minutes cache

	public static Observable<SearchSuggestion> getAllHistoricSearches(Context context) {
		return rxdb(context).query(database(context).query(HistoricSearch.class).orderBy("time desc")).map(SearchSuggestion::fromHistoricSearch);
	}

	public static Observable<SearchSuggestion> getSuggestions(Context context, String query) {
		String whereName = "";
		String whereBeerName = "";
		String[] parts = query.split(" ");
		String[] whereArgs = new String[parts.length];
		for (int i = 0; i < parts.length; i++) {
			whereName += (whereName.length() == 0 ? "" : " and ") + "name like ?";
			whereBeerName += (whereBeerName.length() == 0 ? "" : " and ") + "beerName like ?";
			whereArgs[i] = "%" + parts[i] + "%";
		}

		Observable<HistoricSearch> lastHistoric = rxdb(context).query(
				database(context).query(HistoricSearch.class)
						.withSelection(whereName, whereArgs)
						.orderBy("time desc").limit(2));
		Observable<Brewery> localBrewers = rxdb(context).query(
				database(context).query(Brewery.class)
						.withSelection(whereName, whereArgs)
						.orderBy("name")
						.limit(3));
		Observable<Rating> localRatings = rxdb(context).query(
				database(context).query(Rating.class)
						.withSelection("beerId is not null and (" + whereBeerName + ")", whereArgs)
						.orderBy("timeEntered IS NULL, timeEntered desc")
						.limit(10));
		Observable<Place> localPlaces = rxdb(context).query(
				database(context).query(Place.class)
						.withSelection(whereName, whereArgs)
						.orderBy("rateCount desc")
						.limit(5));
		Observable<Beer> localBeers = rxdb(context).query(
				database(context).query(Beer.class)
						.withSelection(whereName, whereArgs)
						.orderBy("rateCount" + " desc")
						.limit(10));

		return Observable.merge(
				lastHistoric.map(SearchSuggestion::fromHistoricSearch),
				localBrewers.map(SearchSuggestion::fromBrewery),
				localRatings.map(SearchSuggestion::fromRating),
				localPlaces.map(SearchSuggestion::fromPlace),
				localBeers.map(SearchSuggestion::fromBeer))
				.distinct(searchSuggestion -> searchSuggestion.uniqueCode());
	}

	public static Observable<Beer> getBeer(Context context, long beerId) {
		return getBeer(context, beerId, false);
	}

	public static Observable<Beer> getBeer(Context context, long beerId, boolean refresh) {
		Observable<Beer> fresh = api().getBeerDetails(beerId)
				.map(Beer::fromDetails)
				.flatMap(beer -> rxdb(context).putRx(beer));
		if (refresh)
			return fresh;
		else
			return getFresh(
					rxdb(context).get(Beer.class, beerId),
					fresh,
					beer -> isFresh(context, beer.timeCached));
	}

	public static Observable<Rating> getOfflineRating(Context context, long ratingId) {
		return rxdb(context).get(Rating.class, ratingId);
	}

	public static Observable<Rating> getOfflineRatingForBeer(Context context, long beerId) {
		return rxdb(context).query(Rating.class, "beerId = ?", Long.toString(beerId))
				.take(1);
	}

	public static Observable<Rating> getRating(Context context, long beerId, long userId) {
		// @formatter:off
		return getFresh(
				// Local cached value (recent or an offline rating) or...
				rxdb(context).query(Rating.class, "beerId = ?", Long.toString(beerId)),
				// Retrieve fresh value
				Observable.combineLatest(
							getBeer(context, beerId),
							api().getBeerUserRating(beerId, userId),
							rxdb(context).query(Rating.class, "beerId = ?", Long.toString(beerId)).firstOrDefault(null),
							RxTuples.toTriplet())
						// When a value exists online
						.filter(pair -> pair.getValue1() != null)
						// Create new or override existing local rating
						.map(pair -> Rating.fromBeerRating(pair.getValue0(), pair.getValue1(), pair.getValue2()))
						// And store it in the database
						.flatMap(rating -> rxdb(context).putRx(rating)),
				rating -> !rating.isUploaded() || isFresh(context, rating.timeCached));
		// @formatter:on
	}

	public static boolean hasSyncedRatings(Context context) {
		return DatabaseUtils.queryNumEntries(connection(context), Rating.class.getSimpleName()) > 0;
	}

	public static Observable<Rating> getRatings(Context context) {
		return rxdb(context).query(
				database(context).query(Rating.class)
						.orderBy("timeEntered IS NOT NULL, timeEntered desc, timeCached desc"));
	}

	public static Observable<Rating> syncRatings(Context context, Action1<Float> onPageProgress) {
		// Load all user ratings
		return api().getUserRatings(onPageProgress)
				// Map into a local rating
				.map(Rating::fromUserRating)
				.flatMap(rating -> {
					// If the rating already exists in our database, override it
					Rating existing = database(context).query(Rating.class)
							.withSelection("ratingId = ?", rating.ratingId.toString())
							.get();
					if (existing != null)
						rating._id = existing._id;
					return rxdb(context).putRx(rating);
				});
	}

	public static Observable<Rating> postRating(Context context, Rating rating, long userId) {
		// Post live rating
		return api().postRating(rating, userId)
				.flatMap(postedRating ->
						// Combine into a local rating
						Observable.combineLatest(
								getBeer(context, rating.beerId),
								Observable.just(postedRating),
								Observable.just(rating),
								Rating::fromBeerRating))
				// Store in db
				.flatMap(combinedRating -> rxdb(context).putRx(combinedRating))
				// Update the rate count from the server
				.doOnNext(combinedRating -> api().updateUserRateCounts().toBlocking().first());
	}

	public static Observable<Rating> deleteOfflineRating(Context context, Rating rating, long userId) {
		return rxdb(context).deleteRx(rating)
				.flatMap(deletedRating -> {
					if (deletedRating.ratingId == null)
						// Was local only, so we are done
						return Observable.empty();
					else
						// Was stored online, so refresh from the RB server our local rating instance
						return getRating(context, deletedRating.beerId, userId);
				});
	}

	public static Observable<Brewery> getBrewery(Context context, long breweryId) {
		return getBrewery(context, breweryId, false);
	}

	public static Observable<Brewery> getBrewery(Context context, long breweryId, boolean refresh) {
		Observable<Brewery> fresh = api().getBreweryDetails(breweryId)
				.map(Brewery::fromDetails)
				.flatMap(brewery -> rxdb(context).putRx(brewery));
		if (refresh)
			return fresh;
		else
			return getFresh(
					rxdb(context).get(Brewery.class, breweryId),
					fresh,
					brewery -> isFresh(context, brewery.timeCached));
	}

	public static Observable<Place> getPlacesNearby(Context context, Location location) {
		int radius = 40000; // Meters
		// Fresh (from the sever) places are received in a 40 kilometer (±25 mile) radius
		Observable<Place> fresh = api().getPlacesNearby((int) (radius * 0.000621371192D), location.getLatitude(), location.getLongitude())
				.map(Place::fromNearby)
				.flatMap(place -> rxdb(context).putRx(place));
		// Database places are received in a rough rectangular area of 40 kilometers (±0.4 degrees) in each direction
		final double accuracy = radius / 111111;
		String minLat = Double.toString(location.getLatitude() - accuracy);
		String maxLat = Double.toString(location.getLatitude() + accuracy);
		String minLong = Double.toString(location.getLongitude() - accuracy);
		String maxLong = Double.toString(location.getLongitude() + accuracy);
		Observable<Place> db = rxdb(context).query(Place.class, "(latitude BETWEEN ? AND ?) AND (longitude BETWEEN ? AND ?)", minLat, maxLat,
				minLong, maxLong);
		return Observable.merge(fresh, db);
	}

	public static Observable<Place> getPlace(Context context, long placeId) {
		return getPlace(context, placeId, false);
	}

	public static Observable<Place> getPlace(Context context, long placeId, boolean refresh) {
		Observable<Place> fresh = api().getPlaceDetails(placeId)
				.map(Place::fromDetails)
				.flatMap(place -> rxdb(context).putRx(place));
		if (refresh)
			return fresh;
		else
			return getFresh(
					rxdb(context).get(Place.class, placeId),
					fresh,
					place -> isFresh(context, place.timeCached));
	}

	public static Observable<CustomListWithCount> getCustomListsWithCount(Context context) {
		Cursor cursor = connection(context).rawQuery("select l._id, l.name, count(b._id) as beerCount from CustomList as l left outer join " +
				"CustomListBeer as b on b.listId = l._id group by l._id having beerCount > 0 or (l.name is not null and l.name != '')", null);
		return RxCupboard.with(cupboard(), cursor).iterate(CustomListWithCount.class);
	}

	public static Observable<CustomListWithPresence> getCustomLists(Context context, long withBeerId) {
		Cursor cursor = connection(context).rawQuery("select l._id, l.name, (select count(*) from CustomListBeer as b where b.listId = l._id and " +
				"b.beerId = ?) > 0 as hasBeer from CustomList as l", new String[]{Long.toString(withBeerId)});
		return RxCupboard.with(cupboard(), cursor).iterate(CustomListWithPresence.class);
	}

	public static Observable<CustomListBeer> getCustomListBeer(Context context, long listId, long beerId) {
		return rxdb(context).query(CustomListBeer.class, "listId = ? and beerId = ?", Long.toString(listId), Long.toString(beerId));
	}

	public static Observable<CustomListBeer> getCustomListBeers(Context context, long listId) {
		return rxdb(context).query(CustomListBeer.class, "listId = ?", Long.toString(listId));
	}

	public static Observable<DatabaseChange<CustomListBeer>> getCustomListBeerChanges(Context context, long listId) {
		return rxdb(context).changes(CustomListBeer.class)
				.filter(change -> change.entity().listId == listId);
	}

	public static Observable<Integer> deleteCustomList(Context context, CustomList list) {
		return Observable.defer(() ->
				Observable.just(
						database(context).delete(CustomListBeer.class, "listId = ?", Long.toString(list._id))))
				.first()
				.doOnNext(ignore -> rxdb(context).delete(list));
	}

	public static Observable<List<Country>> getCountries(Context context) {
		Observable<Country> db = rxdb(context).query(Country.class);
		Observable<Country> fresh = api().getCountries()
				.map(Country::fromInfo)
				.flatMap(style -> rxdb(context).putRx(style));
		return getFresh(
				db.toSortedList(),
				fresh.toSortedList(),
				stylesList -> stylesList.size() != 0 && isFresh(context, stylesList.get(0).timeCached)
		);
	}

	public static Observable<List<Style>> getStyles(Context context) {
		Observable<Style> db = rxdb(context).query(Style.class);
		Observable<Style> fresh = api().getStyles()
				.map(Style::fromInfo)
				.flatMap(style -> rxdb(context).putRx(style));
		return getFresh(
				db.toSortedList(),
				fresh.toSortedList(),
				stylesList -> stylesList.size() != 0 && isFresh(context, stylesList.get(0).timeCached)
		);
	}

	private static <T> Observable<T> getFresh(Observable<T> db, Observable<T> server, Func1<T, Boolean> isFresh) {
		db = db.filter(item -> item != null);
		return Observable.concat(
				Observable.concat(db, server)
						.takeFirst(isFresh::call),
				db)
				.take(1);
	}

	private static boolean isFresh(Context context, Date timeCached) {
		if (ConnectivityHelper.current(context) == ConnectivityHelper.ConnectivityType.NoConnection)
			return true;
		return timeCached != null && timeCached.after(new Date(System.currentTimeMillis() - MAX_AGE));
	}

	public static boolean clearRatings(Context context) {
		return database(context).delete(Rating.class, "timeEntered is not null") > 0;
	}

}
app/src/main/java/com/ratebeer/android/gui/lists/BarcodeSearchResultsAdapter.java
public class BarcodeSearchResultsAdapter extends ArrayAdapter<BarcodeSearchResult> {

	public BarcodeSearchResultsAdapter(Context context, List<BarcodeSearchResult> results) {
		super(context, android.R.layout.simple_list_item_1, results);
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		TextView view = (TextView) super.getView(position, convertView, parent);
		view.setText(getItem(position).beerName);
		return view;
	}

}
app/src/main/java/com/ratebeer/android/db/CupboardDbHelper.java
public synchronized static DatabaseCompartment database(Context context) {
	if (dbc == null) {
		dbc = cupboardAll.withDatabase(connection(context));
	}
	return dbc;
}
app/src/main/java/com/ratebeer/android/gui/services/SyncService.java
public final class SyncService extends IntentService {

	private static final BehaviorSubject<Boolean> SYNC_STATUS = BehaviorSubject.create(false);
	private static final int NOTIFY_SYNC = 0;
	private static final int REQUEST_SYNC = 0;

	public static Intent start(Context context) {
		return new Intent(context, SyncService.class);
	}

	public static Observable<Boolean> getSyncStatus() {
		return SYNC_STATUS.asObservable();
	}

	public SyncService() {
		super("RateBeer SyncService");
	}

	@Override
	protected void onHandleIntent(Intent intent) {

		if (SYNC_STATUS.toBlocking().first()) {
			return; // Already syncing

		}

		// Perform sync by loading and then storing all of the user's ratings
		final AtomicInteger i = new AtomicInteger();
		Db.syncRatings(this, progress -> reportProgress(true, null, progress))
				.toBlocking()
				.subscribe(
						r -> RBLog.d("R:" + i.getAndIncrement()),
						e -> reportProgress(false, e, null),
						() -> reportProgress(false, null, 100F));

	}

	private void reportProgress(boolean isActive, Throwable error, Float progress) {

		NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

		// Broadcast sync status for external listeners
		SYNC_STATUS.onNext(isActive);

		// Remove persistent notification when done
		if (!isActive && error == null) {
			notificationManager.cancel(NOTIFY_SYNC);
			return;
		}

		// Show error (and allow retry) when sync failed
		if (error != null) {
			PendingIntent retryIntent = PendingIntent.getService(this, REQUEST_SYNC, start(this), PendingIntent.FLAG_CANCEL_CURRENT);
			notificationManager
					.notify(NOTIFY_SYNC, new NotificationCompat.Builder(this)
							.setPriority(NotificationCompat.PRIORITY_DEFAULT)
							.setCategory(NotificationCompat.CATEGORY_ERROR)
							.setAutoCancel(true)
							.setSmallIcon(R.drawable.ic_stat_error)
							.setContentTitle(getString(R.string.sync_error))
							.setContentText(getString(R.string.sync_tryagain))
							.setContentIntent(retryIntent)
					.build());
			return;
		}

		// Show ongoing sync progress
		notificationManager.notify(NOTIFY_SYNC,
				new NotificationCompat.Builder(this)
						.setPriority(NotificationCompat.PRIORITY_LOW)
						.setCategory(NotificationCompat.CATEGORY_SERVICE)
						.setOngoing(true)
						.setSmallIcon(R.drawable.ic_stat_default)
						.setProgress(100, progress.intValue(), false)
						.setContentTitle(getString(R.string.sync_insync))
						.build());

	}

}
app/src/main/java/com/ratebeer/android/gui/lists/PropertiesAdapter.java
public final class PropertiesAdapter extends RecyclerView.Adapter<Property.PropertyHolder> {

	private final List<Property> properties;

	public PropertiesAdapter(List<Property> properties) {
		this.properties = properties;
	}

	@Override
	public Property.PropertyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		return Property.PropertyHolder.build(parent);
	}

	@Override
	public void onBindViewHolder(Property.PropertyHolder holder, int position) {
		holder.bind(properties.get(position));
	}

	@Override
	public int getItemCount() {
		return properties.size();
	}

}
app/src/main/java/com/ratebeer/android/gui/lists/CustomListsAdapter.java
public final class CustomListsAdapter extends RecyclerView.Adapter<CustomListsAdapter.ViewHolder> {

	private List<CustomListWithCount> customLists;

	public CustomListsAdapter(List<CustomListWithCount> customLists) {
		this.customLists = customLists;
	}

	@Override
	public CustomListsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_custom_list, parent, false));
	}

	@Override
	public void onBindViewHolder(ViewHolder holder, int position) {
		CustomListWithCount customList = customLists.get(position);
		holder.countText.setBackgroundResource(ImageUrls.getColor(position));
		holder.countText.setText(String.format(Locale.getDefault(), "%1$d", customList.beerCount));
		holder.nameText.setText(customList.name);
	}

	@Override
	public int getItemCount() {
		return customLists.size();
	}

	public CustomListWithCount get(int position) {
		return customLists.get(position);
	}

	public void update(List<CustomListWithCount> customLists) {
		this.customLists = customLists;
		notifyDataSetChanged();
	}

	static class ViewHolder extends RecyclerView.ViewHolder {

		final View rowLayout;
		final TextView countText;
		final TextView nameText;

		public ViewHolder(View v) {
			super(v);
			rowLayout = v;
			countText = (TextView) v.findViewById(R.id.count_text);
			nameText = (TextView) v.findViewById(R.id.name_text);
		}

	}

}
app/src/main/java/com/ratebeer/android/db/Rating.java
public final class Rating {

	public Long _id;
	public Long ratingId;
	public Long beerId;
	public String beerName;
	public String brewerName;

	public Integer aroma;
	public Integer flavor;
	public Integer mouthfeel;
	public Integer appearance;
	public Integer overall;
	public Float total;
	public String comments;

	public Date timeCached;
	public Date timeEntered;
	public Date timeUpdated;

	public boolean isUploaded() {
		return timeEntered != null;
	}

	public Float calculateTotal() {
		return calculateTotal(aroma, flavor, mouthfeel, appearance, overall);
	}

	@Override
	public String toString() {
		return "[" + _id + "] " + beerName + ": A" + aroma + " A" + appearance + " T" + flavor + " P" + mouthfeel + " O" + overall + " T" + total;
	}

	public static Rating fromUserRating(UserRating userRating) {
		Rating rating = new Rating();
		rating.ratingId = (long) userRating.ratingId;
		rating.beerId = (long) userRating.beerId;
		rating.beerName = userRating.beerName;
		rating.brewerName = userRating.brewerName;

		rating.aroma = userRating.aroma;
		rating.flavor = userRating.flavor;
		rating.mouthfeel = userRating.mouthfeel;
		rating.appearance = userRating.appearance;
		rating.overall = userRating.overall;
		rating.total = userRating.total;
		rating.comments = userRating.comments;

		rating.timeCached = new Date();
		rating.timeEntered = userRating.timeEntered;
		rating.timeUpdated = userRating.timeUpdated;
		return rating;
	}

	public static Rating fromBeerRating(Beer beer, BeerRating beerRating, Rating overrideRating) {
		Rating rating = new Rating();
		rating._id = overrideRating == null? null: overrideRating._id;
		rating.ratingId = (long) beerRating.ratingId;
		rating.beerId = beer._id;
		rating.beerName = beer.name;
		rating.brewerName = beer.brewerName;

		rating.aroma = beerRating.aroma;
		rating.flavor = beerRating.flavor;
		rating.mouthfeel = beerRating.mouthfeel;
		rating.appearance = beerRating.appearance;
		rating.overall = beerRating.overall;
		rating.total = beerRating.total;
		rating.comments = beerRating.comments;

		rating.timeCached = new Date();
		rating.timeEntered = beerRating.timeEntered;
		rating.timeUpdated = beerRating.timeUpdated;
		return rating;
	}

	public static Rating fromOfflineRating(OfflineRating offlineRating) {
		// Convert legacy offline rating into local rating database object
		Rating rating = new Rating();
		rating.ratingId = offlineRating.originalRatingId == null? null: offlineRating.originalRatingId.longValue();
		rating.beerId = offlineRating.beerId == null ? null : offlineRating.beerId.longValue();
		rating.beerName = offlineRating.beerName;

		rating.aroma = offlineRating.aroma;
		rating.flavor = offlineRating.taste;
		rating.mouthfeel = offlineRating.palate;
		rating.appearance = offlineRating.appearance;
		rating.overall = offlineRating.overall;
		rating.total =
				calculateTotal(offlineRating.aroma, offlineRating.taste, offlineRating.palate, offlineRating.appearance, offlineRating.overall);
		rating.comments = offlineRating.comments;

		rating.timeCached = offlineRating.timeSaved;
		return rating;
	}

	public static Float calculateTotal(Integer aroma, Integer flavor, Integer mouthfeel, Integer appearance, Integer overall) {
		if (aroma == null || flavor == null || mouthfeel == null || appearance == null || overall == null)
			return null;
		return (float) (aroma + flavor + mouthfeel + appearance + overall) / 10F;
	}

}
app/src/main/java/com/ratebeer/android/Session.java
public class Session {

	private Context databaseContext;
	private SharedPreferences prefs;
	private StoredSession stored;

	public Observable<StoredSession> getUpdates(Context context, boolean emitInitial) {
		Observable<StoredSession> updates = rxdb(context).changes(StoredSession.class).map(DatabaseChange::entity);
		if (emitInitial)
			updates = updates.startWith(Observable.just(stored));
		return updates;
	}

	private static class Holder {
		static final Session INSTANCE = new Session();
	}

	public static Session get() {
		return Holder.INSTANCE;
	}

	private Session() {
	}

	public void init(Context context) {
		synchronized (this) {
			databaseContext = context.getApplicationContext();
			prefs = PreferenceManager.getDefaultSharedPreferences(context);
			// Resume session from the database
			stored = database(databaseContext).query(StoredSession.class).get();
			if (stored == null) {
				stored = new StoredSession();
			}
		}
	}

	public Context getApplicationContext() {
		return databaseContext;
	}

	public void startSession(int userId, String userName, String password, UserRateCount counts) {
		synchronized (this) {
			stored.userId = userId;
			stored.userName = userName;
			stored.password = password;
			stored.rateCount = counts.rateCount;
			stored.placeCount = counts.placeCount;
			rxdb(databaseContext).put(stored);
		}
	}

	public void updateCounts(UserRateCount counts) {
		synchronized (this) {
			stored.rateCount = counts.rateCount;
			stored.placeCount = counts.placeCount;
			rxdb(databaseContext).put(stored);
		}
	}

	public void endSession() {
		synchronized (this) {
			rxdb(databaseContext).delete(stored);
			stored = new StoredSession();
		}
	}

	public boolean isLoggedIn() {
		return stored.userId != null;
	}

	public Integer getUserId() {
		return stored.userId;
	}

	public String getUserName() {
		return stored.userName;
	}

	public String getPassword() {
		return stored.password;
	}

	public Integer getUserRateCount() {
		return stored.rateCount;
	}

	public boolean isUpgrade() {
		// The "is_first_start" key was used on the old app to identify new installs and thus will always be present on upgrades
		return prefs.contains("is_first_start");
	}

	public boolean hasIgnoredAccount() {
		return prefs.getBoolean("has_ignored_account", false);
	}

	public void registerIgnoreAccount() {
		prefs.edit().putBoolean("has_ignored_account", true).apply();
	}

	public boolean inDataSaverMode() {
		return prefs.getBoolean("data_saver_mode", false);
	}

	public void setDataSaverMode(boolean useDataSaverMode) {
		prefs.edit().putBoolean("data_saver_mode", useDataSaverMode).apply();
	}

	public boolean useMetricUnits() {
		return prefs.getBoolean("units_in_metric", true);
	}

	public void setUseMetricUnits(boolean useMetricUnits) {
		prefs.edit().putBoolean("units_in_metric", useMetricUnits).apply();
	}

}
app/src/main/java/com/ratebeer/android/api/Api.java
public final class Api {

	public static final String DOMAIN = "https://www.ratebeer.com";

	private static final String ENDPOINT = DOMAIN + "/json/";
	private static final String KEY = "tTmwRTWT-W7tpBhtL";
	private static final String COOKIE_USERID = "UserID";
	private static final String COOKIE_SESSIONID = "SessionID";
	private static final long SESSION_TIMEOUT_FORCED = 30 * 60 * 1000; // 30 minute session max until forced sign in
	private static final int RATINGS_PER_PAGE = 100;

	private final Routes routes;
	private final CookieManager cookieManager;
	private long lastSignIn = 0;

	private Api() {

		HttpLoggingInterceptor logging = new HttpLoggingInterceptor(RBLog::v);
		if (BuildConfig.DEBUG)
			logging.setLevel(HttpLoggingInterceptor.Level.BODY);
		cookieManager = new CookieManager(new PersistentCookieStore(), CookiePolicy.ACCEPT_ORIGINAL_SERVER);
		OkHttpClient httpclient = new OkHttpClient.Builder()
				.connectTimeout(8, TimeUnit.SECONDS)
				.writeTimeout(8, TimeUnit.SECONDS)
				.readTimeout(12, TimeUnit.SECONDS)
				.cookieJar(new JavaNetCookieJar(cookieManager))
				.addInterceptor(logging)
				.addNetworkInterceptor(new ResponseInterceptor())
				.build();
		Gson gson = new GsonBuilder()
				.disableHtmlEscaping()
				.registerTypeAdapter(FeedItem.class, new FeedItemDeserializer())
				.registerTypeAdapter(UserInfo.class, new UserInfoDeserializer())
				.registerTypeAdapter(UserRateCount.class, new UserRateCountDeserializer())
				.registerTypeAdapter(UserRating.class, new UserRatingDeserializer())
				.registerTypeAdapter(BeerSearchResult.class, new BeerSearchResultDeserializer())
				.registerTypeAdapter(BarcodeSearchResult.class, new BarcodeSearchResultDeserializer())
				.registerTypeAdapter(BeerDetails.class, new BeerDetailsDeserializer())
				.registerTypeAdapter(BeerRating.class, new BeerRatingDeserializer())
				.registerTypeAdapter(BrewerySearchResult.class, new BrewerySearchResultDeserializer())
				.registerTypeAdapter(BreweryDetails.class, new BreweryDetailsDeserializer())
				.registerTypeAdapter(BreweryBeer.class, new BreweryBeerDeserializer())
				.registerTypeAdapter(PlaceSearchResult.class, new PlaceSearchResultDeserializer())
				.registerTypeAdapter(PlaceNearby.class, new PlaceNearbyDeserializer())
				.registerTypeAdapter(PlaceDetails.class, new PlaceDetailsDeserializer())
				.registerTypeAdapter(PlaceCheckinResult.class, new PlaceCheckinResultDeserializer())
				.registerTypeAdapter(BeerOnTopList.class, new BeerOnTopListDeserializer())
				.registerTypeAdapter(CountryInfo.class, new CountryInfoDeserializer())
				.registerTypeAdapter(StateInfo.class, new StateInfoDeserializer())
				.registerTypeAdapter(StyleInfo.class, new StyleInfoDeserializer())
				.create();
		Retrofit retrofit = new Retrofit.Builder()
				.baseUrl(ENDPOINT)
				.client(httpclient)
				.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
				.addConverterFactory(new HtmlConverterFactory())
				.addConverterFactory(GsonConverterFactory.create(gson))
				.build();
		routes = retrofit.create(Routes.class);

	}

	public static Api get() {
		return Holder.INSTANCE;
	}

	public static Api api() {
		return get();
	}

	private boolean haveLoginCookie() {
		if (cookieManager.getCookieStore().getCookies().isEmpty())
			return false;
		boolean hasUserCookie = false, hasSessionCookie = false;
		for (HttpCookie cookie : cookieManager.getCookieStore().getCookies()) {
			if (cookie.getName().equals(COOKIE_USERID) && !TextUtils.isEmpty(cookie.getValue()) && !cookie.hasExpired())
				hasUserCookie = true;
			if (cookie.getName().equals(COOKIE_SESSIONID) && !TextUtils.isEmpty(cookie.getValue()) && !cookie.hasExpired())
				hasSessionCookie = true;
		}
		return hasUserCookie && hasSessionCookie;
	}

	private boolean isSignedIn() {
		return lastSignIn >= System.currentTimeMillis() - SESSION_TIMEOUT_FORCED && haveLoginCookie();
	}

	private Observable<Boolean> getLoginRoute(String username, String password) {
		return routes.login(username, password, "on").flatMap(result -> {
			if (haveLoginCookie()) {
				return Observable.just(true);
			} else {
				return Observable.error(new IOException("Invalid login response; no session cookies received"));
			}
		});
	}

	/**
	 * A wrapper observable that returns an empty sequence on success such that we can use someLoginDependentCall.startWith(getLoginCookie())
	 */
	private <T> Observable<T> getLoginCookie() {
		return getLoginRoute(Session.get().getUserName(), Session.get().getPassword())
				.subscribeOn(Schedulers.io())
				.flatMap(result -> Observable.empty());
	}

	/**
	 * Performs a login on the server, ensures that the rate counts are updated in our local session and emits true on success
	 */
	public Observable<Boolean> login(String username, String password) {
		// @formatter:off
		return Observable.zip(
					// Combine the latest user counts
					routes.getUserInfo(KEY, username)
							.subscribeOn(Schedulers.newThread())
							.flatMapIterable(infos -> infos)
							.first(),
					// And sign in the user (get login cookies)
					getLoginRoute(username, password)
							.subscribeOn(Schedulers.newThread()),
					(userInfo, loginSuccess) -> userInfo)
				// Then add the user id the user's rate counts
				.flatMap(user -> Observable.zip(
						Observable.just(user),
						routes.getUserRateCount(KEY, user.userId)
								.flatMapIterable(userRateCounts -> userRateCounts),
						RxTuples.toPair()))
				// Store in our own instance the new user data
				.doOnNext(user -> Session.get().startSession(user.getValue0().userId, username, password, user.getValue1()))
				// Store the time of this last successful login to time out the session forcefully after some time
				.doOnNext(ignore -> lastSignIn = System.currentTimeMillis())
				// Return login success
				.map(ignore -> true);
		// @formatter:on
	}

	/**
	 * Calls the server to log out, clear cookies and clear our local session
	 */
	public Observable<Boolean> logout() {
		return routes.logout().map(result -> true)
				.map(success -> cookieManager.getCookieStore().removeAll())
				.doOnNext(success -> Session.get().endSession());
	}

	/**
	 * Retrieves updated rate counts from the server and persists them in our user session before emitting the updated values
	 */
	public Observable<UserRateCount> updateUserRateCounts() {
		return routes.getUserRateCount(KEY, Session.get().getUserId())
				.flatMapIterable(userRateCounts -> userRateCounts)
				.doOnNext(counts -> Session.get().updateCounts(counts));
	}

	/**
	 * Returns an observable sequence (list) of items that appear on the global news feed; does not require user login
	 */
	public Observable<FeedItem> getGlobalFeed() {
		return routes.getFeed(KEY, 1)
				.flatMapIterable(items -> items);
	}

	/**
	 * Returns an observable sequence (list) of items that appear on the local news feed; requires a user to be logged in for its locale
	 */
	public Observable<FeedItem> getLocalFeed() {
		Observable<FeedItem> feed = routes.getFeed(KEY, 2)
				.flatMapIterable(items -> items);
		if (!isSignedIn())
			feed = feed.startWith(getLoginCookie());
		return feed;
	}

	/**
	 * Returns an observable sequence (list) of items that appear on the personalized friends feed; requires a user to be logged in
	 */
	public Observable<FeedItem> getFriendsFeed() {
		Observable<FeedItem> feed = routes.getFeed(KEY, 0).flatMapIterable(items -> items);
		if (!isSignedIn())
			feed = feed.startWith(getLoginCookie());
		return feed;
	}

	/**
	 * Returns an observable sequence (list) of beers (search results) for a text query
	 */
	public Observable<BeerSearchResult> searchBeers(String query) {
		return routes.searchBeers(KEY, Session.get().getUserId(), Normalizer.get().normalizeSearchQuery(query))
				.flatMapIterable(results -> results);
	}

	/**
	 * Returns an observable sequence (list) of beers (search results) for a scanned UPC barcode
	 */
	public Observable<BarcodeSearchResult> searchByBarcode(String barcode) {
		return routes.searchByBarcode(KEY, barcode.trim())
				.flatMapIterable(results -> results);
	}

	/**
	 * Returns the full details for a beer, or throws an exception if it could not be retrieved
	 */
	public Observable<BeerDetails> getBeerDetails(long beerId) {
		return routes.getBeerDetails(KEY, (int) beerId)
				.flatMapIterable(beers -> beers).first();
	}

	/**
	 * Returns a single id of the beer that is aliased to from a certain beer id, or throws an exception if it could not be retrieved
	 */
	public Observable<Long> getBeerAlias(long beerId) {
		return routes.getBeerAlias((int) beerId)
				.filter(alias -> alias != null)
				.first().map(alias -> alias.id);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of the most recent ratings for a beer
	 */
	public Observable<BeerRating> getBeerRatings(long beerId) {
		return routes.getBeerRatings(KEY, (int) beerId, null, 1, 1)
				.flatMapIterable(ratings -> ratings);
	}

	/**
	 * Returns the beer rating of a specific user, or null if the user did not rate it yet
	 */
	public Observable<BeerRating> getBeerUserRating(long beerId, long userId) {
		return routes.getBeerRatings(KEY, (int) beerId, (int) userId, 1, 1)
				.flatMapIterable(ratings -> ratings)
				.firstOrDefault(null);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of all ratings by the logged in user, from most recent to oldest
	 * @param onPageProgress An action, run on the main (UI) thread, which can report sync progress
	 */
	public Observable<UserRating> getUserRatings(Action1<Float> onPageProgress) {
		if (Session.get().getUserId() == null)
			return Observable.empty();
		// Based on the up-to-date rate count, get all pages of ratings necessary and emit them in reverse order
		Observable<Integer> pageCount = routes.getUserRateCount(KEY, Session.get().getUserId())
				.subscribeOn(Schedulers.io())
				.flatMapIterable(counts -> counts)
				.doOnNext(counts -> Session.get().updateCounts(counts))
				.map(counts -> (int) Math.ceil((float) counts.rateCount / RATINGS_PER_PAGE));
		Observable<UserRating> ratings = Observable.combineLatest(
				pageCount,
				pageCount.flatMap(count -> Observable.range(1, count)),
				RxTuples.toPair())
				.flatMap(page -> Observable.combineLatest(
						Observable.just(page),
						routes.getUserRatings(KEY, page.getValue1() + 1),
						RxTuples.toPair()))
				.observeOn(AndroidSchedulers.mainThread())
				.doOnNext(objects -> onPageProgress.call((((float) objects.getValue0().getValue1() + 1) / objects.getValue0().getValue0()) * 100))
				.observeOn(Schedulers.io())
				.flatMapIterable(Pair::getValue1);
		if (!isSignedIn())
			ratings = ratings.startWith(getLoginCookie());
		return ratings;
	}

	/**
	 * Posts or updates a rating and emits the stored rating, as validated on the server side, if the post was successful
	 */
	public Observable<BeerRating> postRating(Rating rating, long userId) {
		Observable<Response<Void>> post;
		// NOTE Manually encode the comments, as RB only accepts ISO-8859-1 encoding here...
		String comments = Normalizer.urlEncode(rating.comments);
		if (rating.ratingId == null)
			post = routes.postRating(rating.beerId.intValue(), rating.aroma, rating.appearance, rating.flavor, rating.mouthfeel, rating.overall,
					comments);
		else
			post = routes.updateRating(rating.beerId.intValue(), rating.ratingId.intValue(), rating.aroma, rating.appearance, rating.flavor,
					rating.mouthfeel, rating.overall, comments);
		return post.flatMap(posted -> routes.getBeerRatings(KEY, rating.beerId.intValue(), (int) userId, 1, 1)
				.flatMapIterable(ratings -> ratings))
				.filter(storedRating -> storedRating.timeEntered != null)
				.first();
	}

	/**
	 * Returns an observable sequence (list) of breweries (search results) for a text query
	 */
	public Observable<BrewerySearchResult> searchBreweries(String query) {
		return routes.searchBreweries(KEY, Normalizer.get().normalizeSearchQuery(query))
				.flatMapIterable(results -> results);
	}

	/**
	 * Returns the full details for a brewery, or throws an exception if it could not be retrieved
	 */
	public Observable<BreweryDetails> getBreweryDetails(long breweryId) {
		return routes.getBreweryDetails(KEY, (int) breweryId)
				.flatMapIterable(breweries -> breweries)
				.first();
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of beers made by some brewery
	 */
	public Observable<BreweryBeer> getBreweryBeers(long breweryId) {
		return routes.getBreweryBeers(KEY, (int) breweryId, Session.get().getUserId())
				.flatMapIterable(beers -> beers);
	}

	/**
	 * Returns an observable sequence (list) of places (search results) for a text query
	 */
	public Observable<PlaceSearchResult> searchPlaces(String query) {
		return routes.searchPlaces(KEY, Normalizer.get().normalizeSearchQuery(query))
				.flatMapIterable(results -> results);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of nearby places
	 */
	public Observable<PlaceNearby> getPlacesNearby(int radius, double latitude, double longitude) {
		return routes.getPlacesNearby(KEY, radius, latitude, longitude)
				.flatMapIterable(places -> places);
	}

	/**
	 * Returns the full details for a place, or throws an exception if it could not be retrieved
	 */
	public Observable<PlaceDetails> getPlaceDetails(long placeId) {
		return routes.getPlaceDetails(KEY, (int) placeId)
				.flatMapIterable(places -> places)
				.first();
	}

	/**
	 * Performs a place check-in on the server and returns true or false to indicate success, or throws an exception if the check-in request failed
	 */
	public Observable<Boolean> performPlaceCheckin(long placeId) {
		Observable<Boolean> checkin = routes.performCheckin(KEY, (int) placeId)
				.map(result -> !TextUtils.isEmpty(result.okResult));
		if (!isSignedIn())
			checkin = checkin.startWith(getLoginCookie());
		return checkin;
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of the top 50 beers overall
	 */
	public Observable<BeerOnTopList> getTopOverall() {
		return routes.getTopOverall(KEY)
				.flatMapIterable(beers -> beers);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of the top 50 beers from a certain country
	 */
	public Observable<BeerOnTopList> getTopByCountry(long countryId) {
		return routes.getTopByCountry(KEY, (int) countryId)
				.flatMapIterable(beers -> beers);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of the top 50 beers from a certain country
	 */
	public Observable<BeerOnTopList> getTopByStyle(long styleId) {
		return routes.getTopByStyle(KEY, (int) styleId)
				.flatMapIterable(beers -> beers);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of countries
	 */
	public Observable<CountryInfo> getCountries() {
		return routes.getCountries(KEY)
				.flatMapIterable(countries -> countries);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of states (of all countries)
	 */
	public Observable<StateInfo> getStates() {
		return routes.getStates(KEY)
				.flatMapIterable(states -> states);
	}

	/**
	 * Returns a (possibly empty) observable sequence (list) of beer styles
	 */
	public Observable<StyleInfo> getStyles() {
		return routes.getStyles(KEY)
				.flatMapIterable(styles -> styles);
	}

	private static class Holder {
		// Holder with static instance which implements a thread safe lazy loading singleton
		static final Api INSTANCE = new Api();
	}

}
app/src/main/java/com/ratebeer/android/gui/widget/ItemClickSupport.java
public final class ItemClickSupport {

	private final RecyclerView mRecyclerView;
	private OnItemClickListener mOnItemClickListener;
	private OnItemLongClickListener mOnItemLongClickListener;

	private View.OnClickListener mOnClickListener = new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			if (mOnItemClickListener != null) {
				RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
				mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
			}
		}

	};

	private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
		@Override
		public boolean onLongClick(View v) {
			if (mOnItemLongClickListener != null) {
				RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
				return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
			}
			return false;
		}
	};

	private RecyclerView.OnChildAttachStateChangeListener mAttachListener = new RecyclerView.OnChildAttachStateChangeListener() {
		@Override
		public void onChildViewAttachedToWindow(View view) {
			if (mOnItemClickListener != null) {
				view.setOnClickListener(mOnClickListener);
			}
			if (mOnItemLongClickListener != null) {
				view.setOnLongClickListener(mOnLongClickListener);
			}
		}

		@Override
		public void onChildViewDetachedFromWindow(View view) {

		}
	};

	private ItemClickSupport(RecyclerView recyclerView) {
		mRecyclerView = recyclerView;
		mRecyclerView.setTag(R.id.item_click_support, this);
		mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
	}

	public static ItemClickSupport addTo(RecyclerView view) {
		ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
		if (support == null) {
			support = new ItemClickSupport(view);
		}
		return support;
	}

	public static ItemClickSupport removeFrom(RecyclerView view) {
		ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
		if (support != null) {
			support.detach(view);
		}
		return support;
	}

	public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
		mOnItemClickListener = listener;
		return this;
	}

	public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
		mOnItemLongClickListener = listener;
		return this;
	}

	private void detach(RecyclerView view) {
		view.removeOnChildAttachStateChangeListener(mAttachListener);
		view.setTag(R.id.item_click_support, null);
	}

	public interface OnItemClickListener {

		void onItemClicked(RecyclerView recyclerView, int position, View v);
	}

	public interface OnItemLongClickListener {

		boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
	}

}
app/src/main/java/com/ratebeer/android/gui/lists/SearchSuggestionsAdapter.java
public final class SearchSuggestionsAdapter extends RecyclerView.Adapter<SearchSuggestionsAdapter.ViewHolder> {

	private final List<SearchSuggestion> searchSuggestions;

	public SearchSuggestionsAdapter() {
		this.searchSuggestions = new ArrayList<>();
	}

	@Override
	public SearchSuggestionsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_search_suggestion, parent, false));
	}

	@Override
	public void onBindViewHolder(ViewHolder holder, int position) {
		SearchSuggestion searchSuggestion = searchSuggestions.get(position);
		int typeResId = 0;
		if (searchSuggestion.type == SearchSuggestion.TYPE_HISTORY) {
			typeResId = R.drawable.ic_type_historic;
		} else if (searchSuggestion.type == SearchSuggestion.TYPE_BREWERY) {
			typeResId = R.drawable.ic_type_brewery;
		} else if (searchSuggestion.type == SearchSuggestion.TYPE_PLACE) {
			typeResId = R.drawable.ic_type_place;
		} else if (searchSuggestion.type == SearchSuggestion.TYPE_BEER) {
			typeResId = R.drawable.ic_type_beer;
		} else if (searchSuggestion.type == SearchSuggestion.TYPE_RATING) {
			typeResId = R.drawable.ic_type_rating;
		}
		holder.typeImage.setImageResource(typeResId);
		holder.nameText.setText(searchSuggestion.suggestion);
		RequestCreator request = null;
		if (searchSuggestion.type == SearchSuggestion.TYPE_BEER && searchSuggestion.itemId != null) {
			request = Images.with(holder.photoImage.getContext()).loadBeer(searchSuggestion.itemId);
		} else if (searchSuggestion.type == SearchSuggestion.TYPE_BREWERY && searchSuggestion.itemId != null) {
			request = Images.with(holder.photoImage.getContext()).loadBrewery(searchSuggestion.itemId);
		}
		if (request != null) {
			holder.photoImage.setVisibility(View.VISIBLE);
			request.placeholder(android.R.color.white).fit().centerInside().into(holder.photoImage);
		} else {
			holder.photoImage.setVisibility(View.GONE);
		}
	}

	@Override
	public int getItemCount() {
		return searchSuggestions.size();
	}

	public SearchSuggestion get(int position) {
		return searchSuggestions.get(position);
	}

	public void update(List<SearchSuggestion> updated) {
		this.searchSuggestions.clear();
		this.searchSuggestions.addAll(updated);
		notifyDataSetChanged();
	}

	static class ViewHolder extends RecyclerView.ViewHolder {

		final ImageView typeImage;
		final TextView nameText;
		final ImageView photoImage;

		public ViewHolder(View v) {
			super(v);
			typeImage = (ImageView) v.findViewById(R.id.type_image);
			nameText = (TextView) v.findViewById(R.id.name_text);
			photoImage = (ImageView) v.findViewById(R.id.photo_image);
		}

	}

}
app/src/main/java/com/ratebeer/android/api/model/FeedItem.java
public final class FeedItem {

	// The different feed item types that are available (with an example LinkText)
	final public static int ITEMTYPE_BEERADDED = 1; // added a new Style Name: <a href="/beer/beer-name/beerid/">Beer Name</a><span class=uaa> (5.0%)
	final public static int ITEMTYPE_BEERRATING = 7; // rated <a href="/beer/beer-name/beerid/userid/">Beer Name</a>
	final public static int ITEMTYPE_PLACERATING = 8; // reviewed <a href="/p/goto/placeid/">Place Name</a>
	final public static int ITEMTYPE_ISDRINKING = 12; // Beer Name
	final public static int ITEMTYPE_EVENTATTENDANCE = 17; // is attending <a href="/event/20713/">Event Name</a> (1/1/2014 in City)
	final public static int ITEMTYPE_AWARD = 18; // Award Name
	final public static int ITEMTYPE_PLACECHECKIN = 20; // checked in at <a href="/p/place-name/placeid/">Place Name, City</a>
	final public static int ITEMTYPE_REACHEDRATINGS = 21; // reached # Style Name ratings!
	final public static int ITEMTYPE_BREWERYADDED = 22; // added a new brewery: <a  href="/brewers/brewer-name/brewerid/">Brewer Name in City</a>

	public int activityId;
	public int userId;
	public String userName;
	public int type;
	public Integer linkId;
	public String linkText;
	public int activityNumber;
	public Date timeEntered;
	public int numComments;

	public Integer getBeerId() {
		if (type == ITEMTYPE_BEERADDED || type == ITEMTYPE_BEERRATING) {
			return linkId == 0? null: linkId;
		}
		// Doe snot apply to a specific beer
		return null;
	}

}
app/src/main/java/com/ratebeer/android/gui/lists/Property.java
public final class Property {

	public int image;
	public String text;
	public View.OnClickListener clickListener;

	public Property() {}

	public Property(int image, String text, View.OnClickListener clickListener) {
		this.image = image;
		this.text = text;
		this.clickListener = clickListener;
	}

	static class PropertyHolder extends RecyclerView.ViewHolder {

		final View view;
		final ImageView propertyImage;
		final TextView propertyText;
		final int staticBackground;
		final int selectableBackground;
		Drawable selectableBackgroundDrawable;
		Property property;

		private PropertyHolder(View v, int staticBackgroundResId, int selectableBackgroundResId) {
			super(v);
			view = v;
			propertyImage = (ImageView) v.findViewById(R.id.property_image);
			propertyText = (TextView) v.findViewById(R.id.property_text);
			staticBackground = staticBackgroundResId;
			selectableBackground = selectableBackgroundResId;
		}

		public static PropertyHolder build(ViewGroup parent) {
			return build(parent, 0, 0);
		}

		public static PropertyHolder build(ViewGroup parent, int staticBackgroundResId) {
			return build(parent, staticBackgroundResId, 0);
		}

		public static PropertyHolder build(ViewGroup parent, int staticBackgroundResId, int selectableBackgroundResId) {
			return new PropertyHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_property, parent, false),
					staticBackgroundResId, selectableBackgroundResId);
		}

		public void bind(Property property) {
			this.property = property;
			propertyImage.setImageResource(property.image);
			propertyText.setText(property.text);
			if (property.clickListener == null) {
				// Remove touch feedback
				view.setOnClickListener(null);
				view.setClickable(false);
				view.setFocusable(false);
				view.setBackgroundResource(staticBackground);
			} else {
				view.setOnClickListener(this.property.clickListener);
				view.setClickable(true);
				view.setFocusable(true);
				if (selectableBackground > 0) {
					view.setBackgroundResource(selectableBackground);
				} else {
					// Set a selectable ripple drawable background
					if (selectableBackgroundDrawable == null) {
						int[] attrs = new int[]{android.R.attr.selectableItemBackground};
						TypedArray ta = view.getContext().obtainStyledAttributes(attrs);
						selectableBackgroundDrawable = ta.getDrawable(0);
						ta.recycle();
					}
					view.setBackgroundDrawable(selectableBackgroundDrawable.getConstantState().newDrawable().mutate());
				}
			}
		}
	}

}
app/src/main/java/com/ratebeer/android/db/views/CustomListWithCount.java
public final class CustomListWithCount implements Comparable<CustomListWithCount> {

	public Long _id;
	public String name;

	public int beerCount;

	@Override
	public int compareTo(@NonNull CustomListWithCount another) {
		return getNonNullName(name).compareTo(getNonNullName(another.name));
	}

	private String getNonNullName(String name) {
		if (name == null)
			return "";
		return name;
	}

}
app/src/main/java/com/ratebeer/android/rx/RxLocation.java
public final class RxLocation {

	private static final long WAITTIME_SHORT = 5000;

	private final ReactiveLocationProvider provider;

	public RxLocation(Context context) {
		provider = new ReactiveLocationProvider(context);
	}

	/**
	 * Returns the last known location, or an empty sequence if none is available
	 */
	@SuppressWarnings("MissingPermission")
	public Observable<Location> getLastLocation() {
		// getLastKnownLocation() completes directly after emitting the last location (which might be null)
		return provider.getLastKnownLocation();
	}

	/**
	 * Returns a fresh and fairly accurate location, or an empty sequence if none is quickly available
	 */
	@SuppressWarnings("MissingPermission")
	public Observable<Location> getQuickLocation() {
		LocationRequest request = LocationRequest.create()
				.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
				.setExpirationDuration(WAITTIME_SHORT);
		return provider.getUpdatedLocation(request)
				.timeout(WAITTIME_SHORT, TimeUnit.MILLISECONDS)
				.take(1)
				.onErrorResumeNext(Observable.empty());
	}

	/**
	 * Returns at most one location, which is the last known location, or when this is not available, a fresh
	 * location, or an empty sequence if none is quickly available
	 */
	public Observable<Location> getLastOrQuickLocation() {
		return getLastLocation().switchIfEmpty(getQuickLocation());
	}

	/**
	 * Returns at most one location, which is fresh and fairly accurate, or the last known location if a new update is
	 * not available quickly, or an empty sequence if no last location is known at all
	 */
	public Observable<Location> getQuickOrLastLocation() {
		return getQuickLocation().switchIfEmpty(getLastLocation());
	}

}
app/src/main/java/com/ratebeer/android/rx/RxViewPager.java
public final class RxViewPager {

	public static Observable<Integer> pageSelected(@NonNull ViewPager view) {
		return Observable.create(new ViewPagerPageSelectedOnSubscribe(view));
	}

}
app/src/main/java/com/ratebeer/android/gui/widget/Animations.java
public final class Animations {

	private static final long EXPAND_COLLAPSE_DURATION = 600; // ms

	public static void fadeFlip(final View in, final View out) {
		ObjectAnimator animOut = ObjectAnimator.ofFloat(out, "alpha", 1F, 0F);
		ObjectAnimator animIn = ObjectAnimator.ofFloat(in, "alpha", 0F, 1F);
		in.setAlpha(0F);
		in.setVisibility(View.VISIBLE);
		in.setTag(View.VISIBLE);
		out.setTag(View.INVISIBLE);
		AnimatorSet animSetXY = new AnimatorSet();
		animSetXY.playTogether(animOut, animIn);
		animSetXY.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationEnd(Animator animation) {
				//noinspection WrongConstant
				out.setVisibility((Integer) out.getTag());
			}
		});
		animSetXY.start();
	}

	public static void fadeFlipOut(final View in, final View out1, final View out2) {
		ObjectAnimator animOut1 = ObjectAnimator.ofFloat(out1, "alpha", 1F, 0F);
		ObjectAnimator animOut2 = ObjectAnimator.ofFloat(out2, "alpha", 1F, 0F);
		ObjectAnimator animIn = ObjectAnimator.ofFloat(in, "alpha", 0F, 1F);
		in.setAlpha(0F);
		in.setVisibility(View.VISIBLE);
		in.setTag(View.VISIBLE);
		out1.setTag(View.INVISIBLE);
		out2.setTag(View.INVISIBLE);
		AnimatorSet animSetXY = new AnimatorSet();
		animSetXY.playTogether(animOut1, animOut2, animIn);
		animSetXY.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationEnd(Animator animation) {
				//noinspection WrongConstant
				out1.setVisibility((Integer) out1.getTag());
				//noinspection WrongConstant
				out2.setVisibility((Integer) out2.getTag());
			}
		});
		animSetXY.start();
	}

	public static void fadeFlipIn(final View in1, final View in2, final View out) {
		ObjectAnimator animOut = ObjectAnimator.ofFloat(out, "alpha", 1F, 0F);
		ObjectAnimator animIn1 = ObjectAnimator.ofFloat(in1, "alpha", 0F, 1F);
		ObjectAnimator animIn2 = ObjectAnimator.ofFloat(in2, "alpha", 0F, 1F);
		in1.setAlpha(0F);
		in2.setAlpha(0F);
		in1.setVisibility(View.VISIBLE);
		in2.setVisibility(View.VISIBLE);
		in1.setTag(View.VISIBLE);
		in2.setTag(View.VISIBLE);
		out.setTag(View.INVISIBLE);
		AnimatorSet animSetXY = new AnimatorSet();
		animSetXY.playTogether(animOut, animIn1);
		animSetXY.playTogether(animOut, animIn2);
		animSetXY.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationEnd(Animator animation) {
				//noinspection WrongConstant
				out.setVisibility((Integer) out.getTag());
			}
		});
		animSetXY.start();
	}

	public static void collapseToTop(View view) {
		final int startHeight = (int) view.getTag(R.id.expand_collapse_height_tag);
		Animation animation = new Animation() {
			@Override
			protected void applyTransformation(float interpolatedTime, Transformation t) {
				super.applyTransformation(interpolatedTime, t);
				view.getLayoutParams().height = (int) (startHeight * (1 - interpolatedTime));
				view.requestLayout();
			}

			@Override
			public boolean willChangeBounds() {
				return true;
			}
		};
		animation.setDuration(EXPAND_COLLAPSE_DURATION);
		animation.setAnimationListener(new Animation.AnimationListener() {
			@Override
			public void onAnimationStart(Animation animation) {

			}

			@Override
			public void onAnimationEnd(Animation animation) {
				view.getLayoutParams().height = startHeight;
				view.setVisibility(View.GONE);
				view.requestLayout();
			}

			@Override
			public void onAnimationRepeat(Animation animation) {

			}
		});
		view.startAnimation(animation);
	}

	public static void expandFromTop(View view) {
		view.setVisibility(View.VISIBLE);
		Animation animation = new Animation() {
			@Override
			protected void applyTransformation(float interpolatedTime, Transformation t) {
				super.applyTransformation(interpolatedTime, t);
				if (view.getTag(R.id.expand_collapse_height_tag) == null)
					view.setTag(R.id.expand_collapse_height_tag, view.getHeight());
				view.getLayoutParams().height = (int) (((int) view.getTag(R.id.expand_collapse_height_tag)) * interpolatedTime);
				view.requestLayout();
			}

			@Override
			public boolean willChangeBounds() {
				return true;
			}
		};
		animation.setDuration(EXPAND_COLLAPSE_DURATION);
		view.startAnimation(animation);
	}

}
app/src/main/java/com/ratebeer/android/gui/lists/FeedItemsAdapter.java
public final class FeedItemsAdapter extends RecyclerView.Adapter<FeedItemsAdapter.ViewHolder> {

	private final Drawable selectableBackgroundDrawable;
	private List<FeedItem> feedItems;

	public FeedItemsAdapter(Context context, List<FeedItem> feedItems) {
		this.feedItems = feedItems;
		int[] attrs = new int[]{android.R.attr.selectableItemBackground};
		TypedArray ta = context.obtainStyledAttributes(attrs);
		selectableBackgroundDrawable = ta.getDrawable(0);
		ta.recycle();
	}

	@Override
	public FeedItemsAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_feed_item, parent, false));
	}

	@Override
	public void onBindViewHolder(ViewHolder holder, int position) {
		FeedItem feedItem = feedItems.get(position);
		Images.with(holder.avatarImage.getContext()).loadUser(feedItem.userName).fit().centerCrop().into(holder.avatarImage);
		if (feedItem.getBeerId() != null) {
			// A clickable row, bound to a specific beer (and show its photo)
			holder.rowLayout.setBackgroundDrawable(selectableBackgroundDrawable.getConstantState().newDrawable().mutate());
			holder.beerImage.setVisibility(View.VISIBLE);
			Images.with(holder.beerImage.getContext()).loadBeer(feedItem.getBeerId()).placeholder(android.R.color.white).fit().centerInside()
					.into(holder.beerImage);
		} else {
			// Not bound to a beer, so not clickable
			holder.rowLayout.setBackgroundResource(0);
			holder.beerImage.setVisibility(View.INVISIBLE);
		}
		holder.activityText.setText(buildActivityText(holder.activityText.getContext(), feedItem));
	}

	private CharSequence buildActivityText(Context context, FeedItem feedItem) {

		// Build a span that contains the user name and the action he/she performed
		SpannableStringBuilder builder = new SpannableStringBuilder();

		// Start with username, in bold
		builder.append(feedItem.userName);
		builder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, feedItem.userName.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
		builder.append(" ");

		// For some activity types, insert the connecting string
		if (feedItem.type == FeedItem.ITEMTYPE_ISDRINKING) {
			builder.append(context.getString(R.string.feed_isdrinking));
			builder.append(" ");
			builder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), builder.length() - 1, builder.length(),
					Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
		} else if (feedItem.type == FeedItem.ITEMTYPE_AWARD) {
			builder.append(context.getString(R.string.feed_wasawarded));
			builder.append(" ");
			builder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), builder.length() - 1, builder.length(),
					Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
		} else if (feedItem.type == FeedItem.ITEMTYPE_PLACECHECKIN) {
			builder.append(context.getString(R.string.feed_checkedinat));
			builder.append(" ");
			builder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), builder.length() - 1, builder.length(),
					Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
		}

		// Parse HTML as spannable from raw link text and replace links with a bold text
		Spanned html = Html.fromHtml(feedItem.linkText);
		builder.append(html);
		URLSpan[] links = builder.getSpans(0, builder.length(), URLSpan.class);
		for (URLSpan link : links) {
			// Make link tags bold (they are not clickable though)
			builder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), builder.getSpanStart(link), builder.getSpanEnd(link),
					builder.getSpanFlags(link));
			builder.removeSpan(link);
		}

		if (feedItem.numComments > 0) {
			builder.append(" ");
			builder.append(context.getString(R.string.feed_andmore, feedItem.numComments));
		}

		return builder;
	}

	@Override
	public int getItemCount() {
		return feedItems.size();
	}

	public FeedItem get(int position) {
		return feedItems.get(position);
	}

	public void update(List<FeedItem> feedItems) {
		this.feedItems = feedItems;
		notifyDataSetChanged();
	}

	static class ViewHolder extends RecyclerView.ViewHolder {

		final View rowLayout;
		final ImageView avatarImage;
		final ImageView beerImage;
		final TextView activityText;

		public ViewHolder(View v) {
			super(v);
			rowLayout = v;
			avatarImage = (ImageView) v.findViewById(R.id.avatar_image);
			beerImage = (ImageView) v.findViewById(R.id.beer_image);
			activityText = (TextView) v.findViewById(R.id.activity_text);
		}

	}

}
app/src/main/java/com/ratebeer/android/gui/lists/LocalPlacesAdapter.java
public final class LocalPlacesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

	private static final int TYPE_HEADER = 0;
	private static final int TYPE_ITEM = 1;

	private final boolean useMetricUnits;
	private final String unitString;
	private List<LocalPlace> places;
	private Map<Marker, Long> markerPlaceIds;

	public LocalPlacesAdapter(Context context, List<LocalPlace> places) {
		this.useMetricUnits = Session.get().useMetricUnits();
		this.unitString = context.getString(useMetricUnits ? R.string.place_km : R.string.place_miles);
		this.places = places;
		MapsInitializer.initialize(context);
	}

	@Override
	public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		if (viewType == TYPE_HEADER)
			return new MapHeaderHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_header_map, parent, false));
		else
			return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_place, parent, false));
	}

	@Override
	public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
		if (holder instanceof MapHeaderHolder) {

			MapHeaderHolder mapHeaderHolder = (MapHeaderHolder) holder;
			mapHeaderHolder.mapView.getMapAsync(googleMap -> {

				// For every place, add a marker and record its position for the camera view bounds
				markerPlaceIds = new HashMap<>(places.size());
				LatLngBounds.Builder bounds = new LatLngBounds.Builder();
				for (LocalPlace place : places) {
					LatLng latLng = new LatLng(place.place.latitude, place.place.longitude);
					bounds.include(latLng);
					Marker marker = googleMap.addMarker(new MarkerOptions().position(latLng).title(place.place.name).icon(BitmapDescriptorFactory
							.defaultMarker(place.place.getTypeMarkerHue())));
					markerPlaceIds.put(marker, place.place._id);
				}

				// Zoom camera to include all markers and allow marker clicks
				int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, mapHeaderHolder.mapView.getResources()
						.getDisplayMetrics());
				//noinspection MissingPermission Already checked permission before loading the places
				googleMap.setMyLocationEnabled(true);
				googleMap.getUiSettings().setMyLocationButtonEnabled(false);
				if (!markerPlaceIds.isEmpty())
					googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), padding));
				googleMap.setOnInfoWindowClickListener(marker -> {
					if (markerPlaceIds != null && markerPlaceIds.get(marker) != null)
						mapHeaderHolder.mapView.getContext().startActivity(PlaceActivity.start(mapHeaderHolder.mapView.getContext(), markerPlaceIds
								.get(marker)));
				});

			});

		} else {

			LocalPlace place = places.get(position - 1);
			ItemHolder itemHolder = (ItemHolder) holder;
			itemHolder.distanceLayout.setBackgroundResource(ImageUrls.getColor(position));
			itemHolder.distanceUnitText.setText(unitString);
			itemHolder.distanceAmountText.setText(String.format(Locale.getDefault(), "%.1f", UnitHelper.asKmOrMiles(place.distance,
					useMetricUnits)));
			itemHolder.placeNameText.setText(place.place.name);
			itemHolder.placeCityText.setText(place.place.city);

		}
	}

	@Override
	public int getItemCount() {
		return places.size() + 1;
	}

	@Override
	public int getItemViewType(int position) {
		return position == 0 ? TYPE_HEADER : TYPE_ITEM;
	}

	public LocalPlace get(int position) {
		if (position == 0)
			return null; // Header, not a rating
		return places.get(position - 1);
	}

	public void update(List<LocalPlace> places) {
		this.places = places;
		notifyDataSetChanged();
	}

	static class MapHeaderHolder extends RecyclerView.ViewHolder {

		final MapView mapView;

		public MapHeaderHolder(View v) {
			super(v);
			mapView = (MapView) v;
			// Call through onCreate and onResume because we had these already but the MapView did not yet exist then
			mapView.onCreate(null);
			mapView.onResume();
			mapView.getMapAsync(googleMap -> googleMap.getUiSettings().setMapToolbarEnabled(false));
		}

	}

	static class ItemHolder extends RecyclerView.ViewHolder {

		final View rowLayout;
		final View distanceLayout;
		final TextView distanceAmountText;
		final TextView distanceUnitText;
		final TextView placeNameText;
		final TextView placeCityText;

		public ItemHolder(View v) {
			super(v);
			rowLayout = v;
			distanceLayout = v.findViewById(R.id.distance_layout);
			distanceAmountText = (TextView) v.findViewById(R.id.distance_amount_text);
			distanceUnitText = (TextView) v.findViewById(R.id.distance_unit_text);
			placeNameText = (TextView) v.findViewById(R.id.place_name_text);
			placeCityText = (TextView) v.findViewById(R.id.place_city_text);
		}

	}

}
app/src/main/java/com/ratebeer/android/gui/lists/LocalPlace.java
public final class LocalPlace implements Comparable<LocalPlace> {

	public Place place;
	public Location placeLocation;
	public Location userLocation;
	public Float distance;

	public static LocalPlace from(Place place, Location userLocation) {
		LocalPlace localPlace = new LocalPlace();
		localPlace.place = place;
		if (place.latitude != null && place.longitude != null) {
			localPlace.placeLocation = new Location("LocalPlace");
			localPlace.placeLocation.setLatitude(place.latitude);
			localPlace.placeLocation.setLongitude(place.longitude);
			localPlace.userLocation = userLocation;
			localPlace.distance = userLocation.distanceTo(localPlace.placeLocation);
		}
		return localPlace;
	}

	@Override
	public int compareTo(LocalPlace another) {
		return Float.compare(distance == null ? Float.MAX_VALUE : distance, another == null || another.distance == null ? Float.MAX_VALUE :
				another.distance);
	}

}
app/src/main/java/com/ratebeer/android/db/HistoricSearch.java
public final class HistoricSearch {

	public Long _id;
	public String name;
	public Date time;

}
app/src/main/java/com/ratebeer/android/gui/lists/SearchSuggestion.java
public final class SearchSuggestion {

	public static final int TYPE_HISTORY = 0;
	public static final int TYPE_BEER = 1;
	public static final int TYPE_RATING = 2;
	public static final int TYPE_BREWERY = 3;
	public static final int TYPE_PLACE = 4;

	public int type;
	public Long itemId;
	public String suggestion;

	private SearchSuggestion() {}

	public static SearchSuggestion fromHistoricSearch(HistoricSearch historicSearch) {
		SearchSuggestion suggestion = new SearchSuggestion();
		// NOTE No item id attached
		suggestion.type = TYPE_HISTORY;
		suggestion.suggestion = historicSearch.name;
		return suggestion;
	}

	public static SearchSuggestion fromBeer(Beer beer) {
		SearchSuggestion suggestion = new SearchSuggestion();
		suggestion.type = TYPE_BEER;
		suggestion.itemId = beer._id;
		suggestion.suggestion = beer.name;
		return suggestion;
	}

	public static SearchSuggestion fromRating(Rating rating) {
		SearchSuggestion suggestion = new SearchSuggestion();
		suggestion.type = TYPE_RATING;
		suggestion.itemId = rating.beerId;
		suggestion.suggestion = rating.beerName;
		return suggestion;
	}

	public static SearchSuggestion fromBrewery(Brewery brewery) {
		SearchSuggestion suggestion = new SearchSuggestion();
		suggestion.type = TYPE_BREWERY;
		suggestion.itemId = brewery._id;
		suggestion.suggestion = brewery.name;
		return suggestion;
	}

	public static SearchSuggestion fromPlace(Place place) {
		SearchSuggestion suggestion = new SearchSuggestion();
		suggestion.type = TYPE_PLACE;
		suggestion.itemId = place._id;
		suggestion.suggestion = place.name;
		return suggestion;
	}

	public String uniqueCode() {
		// A search suggestion is unique per type (but beers and ratings are not) and per id (or suggestion text if no id is available)
		return (type == TYPE_BEER? TYPE_RATING: type) + (itemId != null? Long.toString(itemId): suggestion);
	}

}


Question: import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TabLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.text.InputType;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.jakewharton.rxbinding.support.v7.widget.RxSearchView;
import com.jakewharton.rxbinding.support.v7.widget.SearchViewQueryTextEvent;
import com.jakewharton.rxbinding.view.RxView;
import com.pacoworks.rxtuples.RxTuples;
import com.ratebeer.android.R;
import com.ratebeer.android.Session;
import com.ratebeer.android.api.Api;
import com.ratebeer.android.api.model.FeedItem;
import com.ratebeer.android.db.Db;
import com.ratebeer.android.db.HistoricSearch;
import com.ratebeer.android.db.Rating;
import com.ratebeer.android.db.views.CustomListWithCount;
import com.ratebeer.android.gui.lists.BarcodeSearchResultsAdapter;
import com.ratebeer.android.gui.lists.CustomListsAdapter;
import com.ratebeer.android.gui.lists.FeedItemsAdapter;
import com.ratebeer.android.gui.lists.LocalPlace;
import com.ratebeer.android.gui.lists.LocalPlacesAdapter;
import com.ratebeer.android.gui.lists.PropertiesAdapter;
import com.ratebeer.android.gui.lists.Property;
import com.ratebeer.android.gui.lists.RatingsAdapter;
import com.ratebeer.android.gui.lists.SearchSuggestion;
import com.ratebeer.android.gui.lists.SearchSuggestionsAdapter;
import com.ratebeer.android.gui.services.SyncService;
import com.ratebeer.android.gui.widget.Animations;
import com.ratebeer.android.gui.widget.ItemClickSupport;
import com.ratebeer.android.rx.RxLocation;
import com.ratebeer.android.rx.RxViewPager;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import rx.Observable;
import rx.Subscription;
import rx.schedulers.Schedulers;
import static com.ratebeer.android.db.CupboardDbHelper.database;
					}, e -> {
						Animations.fadeFlip(listsPager, loadingProgress);
						Snackbar.show(this, R.string.error_connectionfailure);
					}, () -> Animations.fadeFlip(listsPager, loadingProgress));

		} else if (type == TAB_MY_LISTS) {

			if (syncSubscription != null)
				syncSubscription.unsubscribe();
			statusText.setVisibility(View.GONE);
			emptyText.setVisibility(View.GONE);
			rateButton.hide();
			listAddButton.show();

			// Show the custom, local user lists
			Animations.fadeFlip(loadingProgress, listsPager);
			ItemClickSupport.addTo(view)
					.setOnItemClickListener((parent, pos, v) -> openCustomList(((CustomListsAdapter) view.getAdapter()).get(pos)));
			Db.getCustomListsWithCount(this)
					.toSortedList()
					.compose(onIoToUi())
					.compose(bindToLifecycle())
					.subscribe(customLists -> {
						if (view.getAdapter() == null)
							view.setAdapter(new CustomListsAdapter(customLists));
						else
							((CustomListsAdapter) view.getAdapter()).update(customLists);
						if (customLists.isEmpty()) {
							emptyText.setVisibility(View.VISIBLE);
							emptyText.setText(R.string.error_nolists);
						}
					}, e -> {
						Animations.fadeFlip(listsPager, loadingProgress);
						Snackbar.show(this, R.string.error_connectionfailure);
					}, () -> Animations.fadeFlip(listsPager, loadingProgress));

		} else if (type == TAB_TOP50) {

			if (syncSubscription != null)
				syncSubscription.unsubscribe();
			statusText.setVisibility(View.GONE);
			emptyText.setVisibility(View.GONE);
			loadingProgress.setVisibility(View.GONE);
			ItemClickSupport.removeFrom(view);
			rateButton.hide();
			listAddButton.hide();

			// Show the links to overall and style top 50s
			List<Property> listLinks = new ArrayList<>();
			listLinks.add(new Property(R.drawable.ic_prop_top, getString(R.string.top_overall),
					v -> startActivity(TopListActivity.start(this, TopListActivity.Mode.OVERALL))));
			listLinks.add(new Property(R.drawable.ic_prop_country, getString(R.string.top_bycountry),
					v -> startActivity(TopListActivity.start(this, TopListActivity.Mode.BY_COUNTRY))));
			listLinks.add(new Property(R.drawable.ic_prop_style, getString(R.string.top_bystyle),
					v -> startActivity(TopListActivity.start(this, TopListActivity.Mode.BY_STYLE))));
			view.setAdapter(new PropertiesAdapter(listLinks));
			listsPager.setVisibility(View.VISIBLE);

		} else {

			if (syncSubscription != null)
				syncSubscription.unsubscribe();
			statusText.setVisibility(View.GONE);
			emptyText.setVisibility(View.GONE);
			rateButton.hide();
			listAddButton.hide();

			// Load a feed (global, local or friends)
			Animations.fadeFlip(loadingProgress, listsPager);
			ItemClickSupport.addTo(view)
					.setOnItemClickListener((parent, pos, v) -> openFeedItem(((FeedItemsAdapter) view.getAdapter()).get(pos)));
			getTabFeed(type)
					.toList()
					.compose(onIoToUi())
					.compose(bindToLifecycle())
					.subscribe(feed -> {
						if (view.getAdapter() == null)
							view.setAdapter(new FeedItemsAdapter(this, feed));
						else
							((FeedItemsAdapter) view.getAdapter()).update(feed);
						if (feed.isEmpty()) {
							emptyText.setVisibility(View.VISIBLE);
							emptyText.setText(R.string.error_noplaces);
						}
					}, e -> {
						Animations.fadeFlip(listsPager, loadingProgress);
						Snackbar.show(this, R.string.error_connectionfailure);
					}, () -> Animations.fadeFlip(listsPager, loadingProgress));

		}
	}

	private Observable<FeedItem> getTabFeed(int type) {
		if (type == TAB_FEED_LOCAL)
			return Api.get().getLocalFeed();
		else if (type == TAB_FEED_FRIENDS)
			return Api.get().getFriendsFeed();
		else
			return Api.get().getGlobalFeed();
	}

	private void openFeedItem(FeedItem feedItem) {
		if (feedItem.getBeerId() != null) {
			startActivity(BeerActivity.start(this, feedItem.getBeerId()));
		}
	}

	private void openRating(Rating rating) {
		if (rating != null && rating.beerId != null && rating.beerId > 0) {
			startActivity(BeerActivity.start(this, rating.beerId));
		} else if (rating != null && !rating.isUploaded()) {
			startActivity(RateActivity.start(this, rating));
		}
	}

	private void openLocalPlace(LocalPlace place) {
		if (place != null)
			startActivity(PlaceActivity.start(this, place.place._id));
	}

Answer: 	private void openCustomList(CustomListWithCount customList) {