/* This file is part of RateBeer For Android. RateBeer for Android is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. RateBeer for Android is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with RateBeer for Android. If not, see <http://www.gnu.org/licenses/>. */ package com.ratebeer.android.gui.fragments; import java.sql.SQLException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import android.content.Intent; import android.os.Bundle; import android.text.Editable; import android.text.SpannableString; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import com.googlecode.androidannotations.annotations.AfterViews; import com.googlecode.androidannotations.annotations.EFragment; import com.googlecode.androidannotations.annotations.FragmentArg; import com.googlecode.androidannotations.annotations.InstanceState; import com.googlecode.androidannotations.annotations.OptionsItem; import com.googlecode.androidannotations.annotations.OptionsMenu; import com.googlecode.androidannotations.annotations.OrmLiteDao; import com.googlecode.androidannotations.annotations.ViewById; import com.j256.ormlite.dao.Dao; import com.ratebeer.android.R; import com.ratebeer.android.api.command.PostRatingCommand; import com.ratebeer.android.app.persistance.DatabaseHelper; import com.ratebeer.android.app.persistance.OfflineRating; import com.ratebeer.android.gui.components.PosterService; import com.ratebeer.android.gui.components.RateBeerFragment; import com.ratebeer.android.gui.fragments.ConfirmDialogFragment.OnDialogResult; import com.ratebeer.android.gui.wheel.IntegerWheelAdapter; import com.ratebeer.android.gui.wheel.IntegerWheelView; import com.ratebeer.android.gui.wheel.OnSelectionChangedListener; import de.neofonie.mobile.app.android.widget.crouton.Crouton; import de.neofonie.mobile.app.android.widget.crouton.Style; @EFragment(R.layout.fragment_rate) @OptionsMenu(R.menu.rate) public class RateFragment extends RateBeerFragment implements Runnable { protected static final int MIN_CHARACTERS = 85; private static final long TIMER_DELAY = 750; private static final int NO_BEER_ID = -1; private static final int NO_OFFLINE_ID = -1; private static final int NO_ORIGINAL_RATING_ID = -1; @FragmentArg @InstanceState protected int beerId = NO_BEER_ID; @FragmentArg @InstanceState protected int originalRatingId = NO_ORIGINAL_RATING_ID; @FragmentArg @InstanceState protected String originalRatingDate = null; @FragmentArg @InstanceState protected String beerName = null; @FragmentArg @InstanceState protected int offlineId = NO_OFFLINE_ID; @FragmentArg @InstanceState protected int aroma; @FragmentArg @InstanceState protected int appearance; @FragmentArg @InstanceState protected int taste; @FragmentArg @InstanceState protected int palate; @FragmentArg @InstanceState protected int overall; @FragmentArg @InstanceState protected String comments = null; @ViewById(R.id.appearance) protected IntegerWheelView appearanceWheel; @ViewById(R.id.aroma) protected IntegerWheelView aromaWheel; @ViewById(R.id.taste) protected IntegerWheelView tasteWheel; @ViewById(R.id.palate) protected IntegerWheelView palateWheel; @ViewById(R.id.overall) protected IntegerWheelView overallWheel; @ViewById(R.id.name) protected TextView nameView; @ViewById(R.id.total) protected TextView totalText; @ViewById protected TextView customnamelabel, characterCounter, assistanceWords; @ViewById protected EditText customname; @ViewById(R.id.comments) protected EditText commentsEdit; @ViewById protected Button addrating, offlineStatus, assistance; @ViewById protected CheckBox share; private Thread timer; @OrmLiteDao(helper = DatabaseHelper.class, model = OfflineRating.class) Dao<OfflineRating, Integer> offlineRatingDao; public RateFragment() { } /** * Resume editing of an offline rating * @param offlineId The database row ID of the offline rating */ public static RateFragment buildFromOfflineID(int offlineId) { return RateFragment_.builder().offlineId(offlineId).build(); } /** * Start an empty beer rating * @param beerName The name of the beer to be rated * @param beerId The ID of the beer to be rated */ public static RateFragment buildFromBeer(String beerName, int beerId) { return RateFragment_.builder().beerName(beerName).beerId(beerId).build(); } /** * Start a beer rating with the fields already populated * @param beerName The name of the beer that was rated * @param beerId The ID of the beer that was rated * @param aroma The aroma rating * @param appearance The appearance rating * @param taste The taste rating * @param palate The palate rating * @param overall The overall rating * @param comments The rating comments */ public static RateFragment buildFromConcrete(String beerName, int beerId, int originalRatingId, String originalRatingDate, int appearance, int aroma, int taste, int palate, int overall, String comments) { return RateFragment_.builder().beerName(beerName).beerId(beerId).originalRatingId(originalRatingId) .originalRatingDate(originalRatingDate).appearance(appearance).aroma(aroma).taste(taste).palate(palate) .overall(overall).comments(comments).build(); } /** * Start a beer rating with the fields already populated * @param extras The rating data to populate the fields with, which at least includes the * PosterService.EXTRA_BEERNAME and PosterService.EXTRA_BEERID */ public static RateFragment buildFromExtras(Bundle extras) { // Assume there is an extra containing the BeerMail object return RateFragment_.buildFromConcrete(extras.getString(PosterService.EXTRA_BEERNAME), extras.getInt(PosterService.EXTRA_BEERID, NO_BEER_ID), extras.getInt(PosterService.EXTRA_ORIGRATINGID, NO_ORIGINAL_RATING_ID), extras.getString(PosterService.EXTRA_ORIGRATINGDATE), extras.getInt(PosterService.EXTRA_AROMA, -1), extras.getInt(PosterService.EXTRA_APPEARANCE, -1), extras.getInt(PosterService.EXTRA_TASTE, -1), extras.getInt(PosterService.EXTRA_PALATE, -1), extras.getInt(PosterService.EXTRA_OVERALL, -1), extras.getString(PosterService.EXTRA_COMMENT)); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_rate, container, false); } @AfterViews public void init() { // Initialize wheels appearanceWheel.setAdapter(new IntegerWheelAdapter(onRatingChanged, 1, 5, "0")); aromaWheel.setAdapter(new IntegerWheelAdapter(onRatingChanged, 1, 10, "0")); tasteWheel.setAdapter(new IntegerWheelAdapter(onRatingChanged, 1, 10, "0")); palateWheel.setAdapter(new IntegerWheelAdapter(onRatingChanged, 1, 5, "0")); overallWheel.setAdapter(new IntegerWheelAdapter(onRatingChanged, 1, 20, "0")); // Initialize fields assistance.setOnClickListener(onAssistanceClick); offlineStatus.setOnClickListener(onOfflineInfoClicked); customname.addTextChangedListener(onCommentChanged); commentsEdit.addTextChangedListener(onCommentChanged); addrating.setOnClickListener(onUploadRating); // Allow clicking of words for rating assistance assistanceWords.setMovementMethod(LinkMovementMethod.getInstance()); String t = getString(R.string.rate_assistance_words); SpannableString s = new SpannableString(t); int start = 0; while (start >= 0) { int comma = t.indexOf(", ", start); if (comma > 0) { s.setSpan(linkWord(t.substring(start, comma)), start, comma, 0); start = comma + 2; } else { s.setSpan(linkWord(t.substring(start, t.length())), start, t.length(), 0); break; } } assistanceWords.setText(s); // Set up offline storage of rating try { offlineStatus.setText(R.string.rate_offline_availble); OfflineRating offline; if (offlineId == NO_OFFLINE_ID && beerId == NO_BEER_ID) { // Nothing pre-known; create a new offline rating offline = new OfflineRating(); offlineRatingDao.create(offline); } else if (offlineId != NO_OFFLINE_ID && beerId == NO_BEER_ID) { // Continue editing an offline-only rating offline = offlineRatingDao.queryForId(offlineId); } else if (offlineId == NO_OFFLINE_ID) { // Beer ID already known but no offline rating yet; try to create an offline rating Map<String, Object> p = new HashMap<String, Object>(); p.put("beerId", beerId); List<OfflineRating> test = offlineRatingDao.queryForFieldValues(p); if (test != null && test.size() > 0) { // Just pick the first; there shouldn't be multiple offline ratings for the same beer offline = test.get(0); } else { // No offline rating yet: create it offline = new OfflineRating(beerId, beerName); offlineRatingDao.create(offline); } } else { // Beer ID known and already an offline rating offline = offlineRatingDao.queryForId(offlineId); } if (offline == null) { // This offline ID is no longer available; rating probably already uploaded Crouton.makeText(getActivity(), R.string.rate_offline_notavailable, Style.ALERT).show(); getFragmentManager().popBackStack(); return; } // Fill fields from the stored offline rating offlineId = offline.getOfflineId(); if (offline.getBeerId() != null) beerId = offline.getBeerId(); if (offline.getBeerName() != null) beerName = offline.getBeerName(); if (offline.getOriginalRatingId() != null) originalRatingId = offline.getOriginalRatingId(); if (offline.getOriginalRatingDate() != null) originalRatingDate = offline.getOriginalRatingDate(); if (offline.getAppearance() != null) appearance = offline.getAppearance(); if (offline.getAroma() != null) aroma = offline.getAroma(); if (offline.getTaste() != null) taste = offline.getTaste(); if (offline.getPalate() != null) palate = offline.getPalate(); if (offline.getOverall() != null) overall = offline.getOverall(); if (offline.getComments() != null) comments = offline.getComments(); if (beerId == NO_BEER_ID) { // Show the offline-only rating screen addrating.setText(R.string.rate_offline_findbeer); nameView.setVisibility(View.GONE); customname.setText(offline.getBeerName()); } else { // Show the normal rating screen customname.setText(beerName); addrating.setText(R.string.rate_addrating); customname.setVisibility(View.GONE); customnamelabel.setVisibility(View.GONE); } } catch (SQLException e) { offlineStatus.setText(R.string.rate_offline_notavailable); } // Check for user; if there is none we cannot upload the rating yet still use the offline feature if (getUser() == null) { addrating.setVisibility(View.GONE); } // Populate field values if these are now known if (aroma > 0) { aromaWheel.getAdapter().setSelectedValue(aroma); } if (appearance > 0) { appearanceWheel.getAdapter().setSelectedValue(appearance); } if (taste > 0) { tasteWheel.getAdapter().setSelectedValue(taste); } if (palate > 0) { palateWheel.getAdapter().setSelectedValue(palate); } if (overall > 0) { overallWheel.getAdapter().setSelectedValue(overall); } if (comments != null) { commentsEdit.setText(comments); } if (beerName != null) { nameView.setText(beerName); } } private ClickableSpan linkWord(final String word) { return new ClickableSpan() { @Override public void onClick(View widget) { // Add the word to the comments String current = commentsEdit.getText().toString(); if (current.length() > 0 && !current.endsWith(" ")) { current += " "; } String toAdd; if (current.length() == 0 || current.endsWith(". ") || current.endsWith("? ") || current.endsWith("! ")) { // Start of new sentence; start with a capital letter toAdd = word.substring(0, 1).toUpperCase() + word.substring(1, word.length()); } else { // Continue a sentence if (!current.endsWith("and ") && !current.endsWith(", ") && current.length() > 1) { current = current.substring(0, current.length() - 1) + ", "; } toAdd = word; } commentsEdit.setText(current + toAdd); commentsEdit.setSelection(commentsEdit.getText().length()); } }; } @OptionsItem(R.id.menu_discard) protected void onDiscardRating() { new ConfirmDialogFragment(new OnDialogResult() { @Override public void onConfirmed() { // Delete the offline version of this rating if (offlineId != NO_OFFLINE_ID) { try { offlineRatingDao.deleteIds(Arrays.asList(offlineId)); } catch (SQLException e) { // Ignore this; we probably don't have access to a database at all } } getFragmentManager().popBackStackImmediate(); } }, R.string.rate_offline_confirmdiscard).show(getFragmentManager(), "dialog"); } private OnClickListener onAssistanceClick = new OnClickListener() { @Override public void onClick(View v) { assistance.setText(assistanceWords.getVisibility() == View.VISIBLE ? R.string.rate_showassistance : R.string.rate_hideassistance); assistanceWords.setVisibility(assistanceWords.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); } }; private OnClickListener onOfflineInfoClicked = new OnClickListener() { @Override public void onClick(View arg0) { load(TextInfoFragment_.builder().title(getString(R.string.rate_offline_title)) .info(getString(R.string.rate_offline_info)).build()); } }; /* * @Override public void onResume() { super.onResume(); getRateBeerActivity().getActionBar().setTitle(beerName); * getRateBeerActivity().getActionBar().withHomeButton(); } */ private OnSelectionChangedListener<Integer> onRatingChanged = new OnSelectionChangedListener<Integer>() { @Override public void onSelectionChanged(Integer newSelection) { int appearance = appearanceWheel.getAdapter().getSelectedValue(); int aroma = aromaWheel.getAdapter().getSelectedValue(); int taste = tasteWheel.getAdapter().getSelectedValue(); int palate = palateWheel.getAdapter().getSelectedValue(); int overall = overallWheel.getAdapter().getSelectedValue(); totalText.setText(Float.toString(PostRatingCommand .calculateTotal(aroma, appearance, taste, palate, overall))); timer = new Thread(RateFragment.this); timer.start(); } }; private TextWatcher onCommentChanged = new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { timer = new Thread(RateFragment.this); timer.start(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { if (timer != null && timer.isAlive()) { timer.interrupt(); } } @Override public void afterTextChanged(Editable s) { if (getActivity() == null) return; int left = MIN_CHARACTERS - commentsEdit.getText().length(); if (commentsEdit.getText().length() == 0) { characterCounter.setText(R.string.rate_commenttooshort); } else if (left >= 0) { characterCounter.setText(getString(R.string.rate_charstogo, Integer.toString(left))); } else { characterCounter.setText(R.string.rate_commentok); } } }; /** * Implements a small timer to delay the loading of the example RSS feed */ @Override public void run() { try { Thread.sleep(TIMER_DELAY); // If not interrupted... if (getActivity() == null) return; getActivity().runOnUiThread(new Runnable() { @Override public void run() { // Store the updated offline rating in the database saveOfflineRating(); } }); } catch (InterruptedException e) { } } private void saveOfflineRating() { // Gather the rating data String customName = customname.getText().toString(); int appearance = appearanceWheel.getAdapter().getSelectedValue(); int aroma = aromaWheel.getAdapter().getSelectedValue(); int taste = tasteWheel.getAdapter().getSelectedValue(); int palate = palateWheel.getAdapter().getSelectedValue(); int overall = overallWheel.getAdapter().getSelectedValue(); String comments = commentsEdit.getText().toString(); try { if (getActivity() == null) { offlineStatus.setText(R.string.rate_offline_notavailable); return; } // Get the offline rating from the database OfflineRating offline = offlineRatingDao.queryForId(offlineId); if (offline == null) { offlineStatus.setText(R.string.rate_offline_notavailable); return; } // Update the databse object offline.update(beerId, customName, originalRatingId, originalRatingDate, appearance, aroma, taste, palate, overall, comments); offlineRatingDao.update(offline); offlineStatus.setText(R.string.rate_offline_availble); } catch (SQLException e) { offlineStatus.setText(R.string.rate_offline_notavailable); } } private OnClickListener onUploadRating = new OnClickListener() { @Override public void onClick(View v) { if (beerId == NO_BEER_ID) { findRatedBeer(); } else { postRating(); } } }; protected void findRatedBeer() { load(FindRatedBeerFragment_.builder().offlineId(offlineId).build()); } protected void postRating() { // Try to submit rating to the poster service String comment = commentsEdit.getText().toString(); if (comment != null && comment.length() > MIN_CHARACTERS) { try { int appearance = appearanceWheel.getAdapter().getSelectedValue(); int aroma = aromaWheel.getAdapter().getSelectedValue(); int taste = tasteWheel.getAdapter().getSelectedValue(); int palate = palateWheel.getAdapter().getSelectedValue(); int overall = overallWheel.getAdapter().getSelectedValue(); // Use the poster service to post this new rating Intent i = new Intent(PosterService.ACTION_POSTRATING); i.putExtra(PosterService.EXTRA_BEERID, beerId); i.putExtra(PosterService.EXTRA_OFFLINEID, offlineId); i.putExtra(PosterService.EXTRA_ORIGRATINGID, originalRatingId); i.putExtra(PosterService.EXTRA_ORIGRATINGDATE, originalRatingDate); i.putExtra(PosterService.EXTRA_BEERNAME, beerName); i.putExtra(PosterService.EXTRA_AROMA, aroma); i.putExtra(PosterService.EXTRA_APPEARANCE, appearance); i.putExtra(PosterService.EXTRA_TASTE, taste); i.putExtra(PosterService.EXTRA_PALATE, palate); i.putExtra(PosterService.EXTRA_OVERALL, overall); i.putExtra(PosterService.EXTRA_COMMENT, comment); getActivity().startService(i); // Share this rating? if (share.isChecked()) { Intent s = new Intent(Intent.ACTION_SEND); s.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); s.setType("text/plain"); s.putExtra(Intent.EXTRA_TEXT, String.format(getSettings().getRatingShareText(), Integer.toString(beerId), beerName, PostRatingCommand.calculateTotal(aroma, appearance, taste, palate, overall), Integer.toString(getUser().getUserID()))); startActivity(Intent.createChooser(s, getString(R.string.app_sharerating))); } // Close this fragment and open add availability screen getFragmentManager().popBackStack(); load(AddAvailabilityFragment_.builder().beerId(beerId).beerName(beerName).build()); } catch (NumberFormatException e) { publishException(null, getText(R.string.rate_ratingnotcompleted).toString()); } } else { publishException(null, getText(R.string.rate_commenttooshort).toString()); } } }