package cgeo.geocaching.log; import cgeo.geocaching.Intents; import cgeo.geocaching.R; import cgeo.geocaching.activity.Keyboard; import cgeo.geocaching.connector.ConnectorFactory; import cgeo.geocaching.connector.LogResult; import cgeo.geocaching.connector.trackable.AbstractTrackableLoggingManager; import cgeo.geocaching.connector.trackable.TrackableBrand; import cgeo.geocaching.connector.trackable.TrackableConnector; import cgeo.geocaching.connector.trackable.TrackableTrackingCode; import cgeo.geocaching.enumerations.LoadFlags; import cgeo.geocaching.enumerations.Loaders; import cgeo.geocaching.enumerations.StatusCode; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.log.LogTemplateProvider.LogContext; import cgeo.geocaching.log.LogTemplateProvider.LogTemplate; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Trackable; import cgeo.geocaching.network.AndroidBeam; import cgeo.geocaching.search.AutoCompleteAdapter; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.settings.SettingsActivity; import cgeo.geocaching.storage.DataStore; import cgeo.geocaching.twitter.Twitter; import cgeo.geocaching.ui.dialog.CoordinatesInputDialog; import cgeo.geocaching.ui.dialog.CoordinatesInputDialog.CoordinateUpdate; import cgeo.geocaching.ui.dialog.DateDialog; import cgeo.geocaching.ui.dialog.DateDialog.DateDialogParent; import cgeo.geocaching.ui.dialog.Dialogs; import cgeo.geocaching.ui.dialog.TimeDialog; import cgeo.geocaching.ui.dialog.TimeDialog.TimeDialogParent; import cgeo.geocaching.utils.AndroidRxUtils; import cgeo.geocaching.utils.AsyncTaskWithProgress; import cgeo.geocaching.utils.Formatter; import cgeo.geocaching.utils.Log; import cgeo.geocaching.utils.functions.Func1; import android.R.layout; import android.R.string; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnFocusChangeListener; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.functions.Action; import io.reactivex.functions.Consumer; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; public class LogTrackableActivity extends AbstractLoggingActivity implements DateDialogParent, TimeDialogParent, CoordinateUpdate, LoaderManager.LoaderCallbacks<List<LogTypeTrackable>> { @BindView(R.id.type) protected Button typeButton; @BindView(R.id.date) protected Button dateButton; @BindView(R.id.time) protected Button timeButton; @BindView(R.id.geocode) protected AutoCompleteTextView geocodeEditText; @BindView(R.id.coordinates) protected Button coordinatesButton; @BindView(R.id.tracking) protected EditText trackingEditText; @BindView(R.id.log) protected EditText logEditText; @BindView(R.id.tweet) protected CheckBox tweetCheck; @BindView(R.id.tweet_box) protected LinearLayout tweetBox; private final CompositeDisposable createDisposables = new CompositeDisposable(); private List<LogTypeTrackable> possibleLogTypesTrackable = new ArrayList<>(); private String geocode = null; private Geopoint geopoint; private Geocache geocache = new Geocache(); /** * As long as we still fetch the current state of the trackable from the Internet, the user cannot yet send a log. */ private boolean postReady = true; private Calendar date = Calendar.getInstance(); private LogTypeTrackable typeSelected = LogTypeTrackable.getById(Settings.getTrackableAction()); private Trackable trackable; private TrackableBrand brand; String trackingCode; TrackableConnector connector; private AbstractTrackableLoggingManager loggingManager; /** * How many times the warning popup for geocode not set should be displayed */ public static final int MAX_SHOWN_POPUP_TRACKABLE_WITHOUT_GEOCODE = 3; public static final int LOG_TRACKABLE = 1; @Override public Loader<List<LogTypeTrackable>> onCreateLoader(final int id, final Bundle bundle) { showProgress(true); if (id == Loaders.LOGGING_TRAVELBUG.getLoaderId()) { loggingManager.setGuid(trackable.getGuid()); } return loggingManager; } @Override public void onLoadFinished(final Loader<List<LogTypeTrackable>> listLoader, final List<LogTypeTrackable> logTypesTrackable) { if (CollectionUtils.isNotEmpty(logTypesTrackable)) { possibleLogTypesTrackable.clear(); possibleLogTypesTrackable.addAll(logTypesTrackable); } if (logTypesTrackable != null && !logTypesTrackable.contains(typeSelected) && !logTypesTrackable.isEmpty()) { setType(logTypesTrackable.get(0)); showToast(res.getString(R.string.info_log_type_changed)); } postReady = loggingManager.postReady(); // we're done, user can post log showProgress(false); } @Override public void onLoaderReset(final Loader<List<LogTypeTrackable>> listLoader) { // nothing } @Override public void onCreate(final Bundle savedInstanceState) { onCreate(savedInstanceState, R.layout.logtrackable_activity); ButterKnife.bind(this); // get parameters final Bundle extras = getIntent().getExtras(); final Uri uri = AndroidBeam.getUri(getIntent()); if (extras != null) { geocode = extras.getString(Intents.EXTRA_GEOCODE); // Load geocache if we can if (StringUtils.isNotBlank(extras.getString(Intents.EXTRA_GEOCACHE))) { final Geocache tmpGeocache = DataStore.loadCache(extras.getString(Intents.EXTRA_GEOCACHE), LoadFlags.LOAD_CACHE_OR_DB); if (tmpGeocache != null) { geocache = tmpGeocache; } } // Load Tracking Code if (StringUtils.isNotBlank(extras.getString(Intents.EXTRA_TRACKING_CODE))) { trackingCode = extras.getString(Intents.EXTRA_TRACKING_CODE); } } // try to get data from URI if (geocode == null && uri != null) { geocode = ConnectorFactory.getTrackableFromURL(uri.toString()); } // try to get data from URI from a potential tracking Code if (geocode == null && uri != null) { final TrackableTrackingCode tbTrackingCode = ConnectorFactory.getTrackableTrackingCodeFromURL(uri.toString()); if (!tbTrackingCode.isEmpty()) { brand = tbTrackingCode.brand; geocode = tbTrackingCode.trackingCode; } } // no given data if (geocode == null) { showToast(res.getString(R.string.err_tb_display)); finish(); return; } refreshTrackable(); } private void refreshTrackable() { showProgress(true); // create trackable connector connector = ConnectorFactory.getTrackableConnector(geocode, brand); loggingManager = connector.getTrackableLoggingManager(this); if (loggingManager == null) { showToast(res.getString(R.string.err_tb_not_loggable)); finish(); } // Initialize the UI init(); createDisposables.add(AndroidRxUtils.bindActivity(this, ConnectorFactory.loadTrackable(geocode, null, null, brand)).subscribe(new Consumer<Trackable>() { @Override public void accept(final Trackable newTrackable) { if (trackingCode != null) { newTrackable.setTrackingcode(trackingCode); } startLoader(newTrackable); } }, new Consumer<Throwable>() { @Override public void accept(final Throwable throwable) throws Exception { Log.e("refreshTrackable", throwable); } }, new Action() { @Override public void run() throws Exception { startLoader(null); } })); } private void startLoader(final Trackable newTrackable) { trackable = newTrackable; // Start loading in background getSupportLoaderManager().initLoader(connector.getTrackableLoggingManagerLoaderId(), null, LogTrackableActivity.this).forceLoad(); displayTrackable(); } private void displayTrackable() { if (trackable == null) { Log.e("LogTrackableActivity.onCreate, cannot load trackable: " + geocode); showProgress(false); if (StringUtils.isNotBlank(geocode)) { showToast(res.getString(R.string.err_tb_find) + ' ' + geocode + '.'); } else { showToast(res.getString(R.string.err_tb_find_that)); } setResult(RESULT_CANCELED); finish(); return; } // We're in LogTrackableActivity, so trackable must be loggable ;) if (!trackable.isLoggable()) { showProgress(false); showToast(res.getString(R.string.err_tb_not_loggable)); finish(); return; } setTitle(res.getString(R.string.trackable_touch) + ": " + StringUtils.defaultIfBlank(trackable.getGeocode(), trackable.getName())); // Display tracking code if we have, and move cursor next if (trackingCode != null) { trackingEditText.setText(trackingCode); Dialogs.moveCursorToEnd(trackingEditText); } init(); // only after loading we know which menu items for smileys need to be created invalidateOptionsMenuCompatible(); showProgress(false); requestKeyboardForLogging(); } @Override protected void requestKeyboardForLogging() { if (StringUtils.isBlank(trackingEditText.getText())) { new Keyboard(this).show(trackingEditText); } else { super.requestKeyboardForLogging(); } } @Override public void onConfigurationChanged(final Configuration newConfig) { super.onConfigurationChanged(newConfig); init(); } @Override public void onCreateContextMenu(final ContextMenu menu, final View view, final ContextMenu.ContextMenuInfo info) { super.onCreateContextMenu(menu, view, info); final int viewId = view.getId(); if (viewId == R.id.type) { for (final LogTypeTrackable typeOne : possibleLogTypesTrackable) { menu.add(viewId, typeOne.id, 0, typeOne.getLabel()); } } } @Override public boolean onContextItemSelected(final MenuItem item) { final int group = item.getGroupId(); final int id = item.getItemId(); if (group == R.id.type) { setType(LogTypeTrackable.getById(id)); return true; } return false; } private void init() { registerForContextMenu(typeButton); typeButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View view) { openContextMenu(view); } }); setType(typeSelected); dateButton.setOnClickListener(new DateListener()); setDate(date); // show/hide Time selector if (loggingManager.canLogTime()) { timeButton.setOnClickListener(new TimeListener()); setTime(date); timeButton.setVisibility(View.VISIBLE); } else { timeButton.setVisibility(View.GONE); } // Register Coordinates Listener if (loggingManager.canLogCoordinates()) { geocodeEditText.setOnFocusChangeListener(new LoadGeocacheListener()); geocodeEditText.setText(geocache.getGeocode()); updateCoordinates(geocache.getCoords()); coordinatesButton.setOnClickListener(new CoordinatesListener()); } initTwitter(); if (CollectionUtils.isEmpty(possibleLogTypesTrackable)) { possibleLogTypesTrackable = Trackable.getPossibleLogTypes(); } disableSuggestions(trackingEditText); initGeocodeSuggestions(); } /** * Link the geocodeEditText to the SuggestionsGeocode. */ private void initGeocodeSuggestions() { geocodeEditText.setAdapter(new AutoCompleteAdapter(geocodeEditText.getContext(), layout.simple_dropdown_item_1line, new Func1<String, String[]>() { @Override public String[] call(final String input) { return DataStore.getSuggestionsGeocode(input); } })); } @Override public void setDate(final Calendar dateIn) { date = dateIn; dateButton.setText(Formatter.formatShortDateVerbally(date.getTime().getTime())); } @Override public void setTime(final Calendar dateIn) { date = dateIn; timeButton.setText(Formatter.formatTime(date.getTime().getTime())); } public void setType(final LogTypeTrackable type) { typeSelected = type; typeButton.setText(typeSelected.getLabel()); // show/hide Tracking Code Field for note type if (typeSelected != LogTypeTrackable.NOTE || loggingManager.isTrackingCodeNeededToPostNote()) { trackingEditText.setVisibility(View.VISIBLE); // Request focus if field is empty if (StringUtils.isBlank(trackingEditText.getText())) { trackingEditText.requestFocus(); } } else { trackingEditText.setVisibility(View.GONE); } // show/hide Coordinate fields as Trackable needs if (LogTypeTrackable.isCoordinatesNeeded(typeSelected) && loggingManager.canLogCoordinates()) { geocodeEditText.setVisibility(View.VISIBLE); coordinatesButton.setVisibility(View.VISIBLE); // Request focus if field is empty if (StringUtils.isBlank(geocodeEditText.getText())) { geocodeEditText.requestFocus(); } } else { geocodeEditText.setVisibility(View.GONE); coordinatesButton.setVisibility(View.GONE); } } private void initTwitter() { tweetCheck.setChecked(true); if (Settings.isUseTwitter() && Settings.isTwitterLoginValid()) { tweetBox.setVisibility(View.VISIBLE); } else { tweetBox.setVisibility(View.GONE); } } @Override public void updateCoordinates(final Geopoint geopointIn) { if (geopointIn == null) { return; } geopoint = geopointIn; coordinatesButton.setText(geopoint.toString()); geocache.setCoords(geopoint); } private class DateListener implements View.OnClickListener { @Override public void onClick(final View arg0) { final DateDialog dateDialog = DateDialog.getInstance(date); dateDialog.setCancelable(true); dateDialog.show(getSupportFragmentManager(), "date_dialog"); } } private class TimeListener implements View.OnClickListener { @Override public void onClick(final View arg0) { final TimeDialog timeDialog = TimeDialog.getInstance(date); timeDialog.setCancelable(true); timeDialog.show(getSupportFragmentManager(), "time_dialog"); } } private class CoordinatesListener implements View.OnClickListener { @Override public void onClick(final View arg0) { final CoordinatesInputDialog coordinatesDialog = CoordinatesInputDialog.getInstance(geocache, geopoint); coordinatesDialog.setCancelable(true); coordinatesDialog.show(getSupportFragmentManager(), "coordinates_dialog"); } } // React when changing geocode private class LoadGeocacheListener implements OnFocusChangeListener { @Override public void onFocusChange(final View view, final boolean hasFocus) { if (!hasFocus && StringUtils.isNotBlank(geocodeEditText.getText())) { final Geocache tmpGeocache = DataStore.loadCache(geocodeEditText.getText().toString(), LoadFlags.LOAD_CACHE_OR_DB); if (tmpGeocache == null) { geocache.setGeocode(geocodeEditText.getText().toString()); } else { geocache = tmpGeocache; updateCoordinates(geocache.getCoords()); } } } } private class Poster extends AsyncTaskWithProgress<String, StatusCode> { Poster(final Activity activity, final String progressMessage) { super(activity, null, progressMessage, true); } @Override protected StatusCode doInBackgroundInternal(final String[] params) { final String logMsg = params[0]; try { // Set selected action final TrackableLog trackableLog = new TrackableLog(trackable.getGeocode(), trackable.getTrackingcode(), trackable.getName(), 0, 0, trackable.getBrand()); trackableLog.setAction(typeSelected); // Real call to post log final LogResult logResult = loggingManager.postLog(geocache, trackableLog, date, logMsg); // Now posting tweet if log is OK if (logResult.getPostLogResult() == StatusCode.NO_ERROR) { addLocalTrackableLog(logMsg); if (tweetCheck.isChecked() && tweetBox.getVisibility() == View.VISIBLE) { // TODO oldLogType as a temp workaround... final LogEntry logNow = new LogEntry.Builder() .setDate(date.getTimeInMillis()) .setLogType(typeSelected.oldLogtype) .setLog(logMsg) .build(); Twitter.postTweetTrackable(trackable.getGeocode(), logNow); } } // Display errors to the user if (StringUtils.isNotEmpty(logResult.getLogId())) { showToast(logResult.getLogId()); } // Return request status return logResult.getPostLogResult(); } catch (final RuntimeException e) { Log.e("LogTrackableActivity.Poster.doInBackgroundInternal", e); } return StatusCode.LOG_POST_ERROR; } @Override protected void onPostExecuteInternal(final StatusCode status) { if (status == StatusCode.NO_ERROR) { showToast(res.getString(R.string.info_log_posted)); finish(); } else if (status == StatusCode.LOG_SAVED) { // is this part of code really reachable? Didn't see StatusCode.LOG_SAVED in postLog() showToast(res.getString(R.string.info_log_saved)); finish(); } else { showToast(status.getErrorString(res)); } } /** * Adds the new log to the list of log entries for this trackable to be able to show it in the trackable * activity. * * */ private void addLocalTrackableLog(final String logText) { // TODO create a LogTrackableEntry. For now use "oldLogtype" as a temporary migration path final LogEntry logEntry = new LogEntry.Builder() .setDate(date.getTimeInMillis()) .setLogType(typeSelected.oldLogtype) .setLog(logText) .build(); final List<LogEntry> modifiedLogs = new ArrayList<>(trackable.getLogs()); modifiedLogs.add(0, logEntry); trackable.setLogs(modifiedLogs); DataStore.saveTrackable(trackable); } } public static Intent getIntent(final Context context, final Trackable trackable) { final Intent logTouchIntent = new Intent(context, LogTrackableActivity.class); logTouchIntent.putExtra(Intents.EXTRA_GEOCODE, trackable.getGeocode()); logTouchIntent.putExtra(Intents.EXTRA_TRACKING_CODE, trackable.getTrackingcode()); return logTouchIntent; } public static Intent getIntent(final Context context, final Trackable trackable, final String geocache) { final Intent logTouchIntent = getIntent(context, trackable); logTouchIntent.putExtra(Intents.EXTRA_GEOCACHE, geocache); return logTouchIntent; } @Override protected LogContext getLogContext() { return new LogContext(trackable, null); } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_send: if (connector.isRegistered()) { sendLog(); } else { // Redirect user to concerned connector settings Dialogs.confirmYesNo(this, res.getString(R.string.settings_title_open_settings), res.getString(R.string.err_trackable_log_not_anonymous, trackable.getBrand().getLabel(), connector.getServiceTitle()), new OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { if (connector.getPreferenceActivity() > 0) { SettingsActivity.openForScreen(connector.getPreferenceActivity(), LogTrackableActivity.this); } else { showToast(res.getString(R.string.err_trackable_no_preference_activity)); } } }); } return true; default: break; } return super.onOptionsItemSelected(item); } /** * Do form validation then post the Log */ private void sendLog() { // Can logging? if (!postReady) { showToast(res.getString(R.string.log_post_not_possible)); return; } // Check Tracking Code existence if (loggingManager.isTrackingCodeNeededToPostNote() && trackingEditText.getText().toString().isEmpty()) { showToast(res.getString(R.string.err_log_post_missing_tracking_code)); return; } trackable.setTrackingcode(trackingEditText.getText().toString()); // Check params for trackables needing coordinates if (loggingManager.canLogCoordinates() && LogTypeTrackable.isCoordinatesNeeded(typeSelected) && geopoint == null) { showToast(res.getString(R.string.err_log_post_missing_coordinates)); return; } // Some Trackable connectors recommend logging with a Geocode. // Note: Currently, counter is shared between all connectors recommending Geocode. if (LogTypeTrackable.isCoordinatesNeeded(typeSelected) && loggingManager.canLogCoordinates() && connector.recommendLogWithGeocode() && geocodeEditText.getText().toString().isEmpty() && Settings.getLogTrackableWithoutGeocodeShowCount() < MAX_SHOWN_POPUP_TRACKABLE_WITHOUT_GEOCODE) { new LogTrackableWithoutGeocodeBuilder().create(this).show(); } else { postLog(); } } /** * Post Log in Background */ private void postLog() { new Poster(this, res.getString(R.string.log_saving)).execute(logEditText.getText().toString()); Settings.setTrackableAction(typeSelected.id); Settings.setLastTrackableLog(logEditText.getText().toString()); } @Override public boolean onCreateOptionsMenu(final Menu menu) { final boolean result = super.onCreateOptionsMenu(menu); for (final LogTemplate template : LogTemplateProvider.getTemplatesWithoutSignature()) { if (template.getTemplateString().equals("NUMBER")) { menu.findItem(R.id.menu_templates).getSubMenu().removeItem(template.getItemId()); } } return result; } @Override protected String getLastLog() { return Settings.getLastTrackableLog(); } /** * This will display a popup for confirming if Trackable Log should be send without a geocode. * It will be displayed only MAX_SHOWN_POPUP_TRACKABLE_WITHOUT_GEOCODE times. A "Do not ask me again" * checkbox is also added. */ public class LogTrackableWithoutGeocodeBuilder { private CheckBox doNotAskAgain; public AlertDialog create(final Activity activity) { final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.trackable_title_log_without_geocode); final View layout = View.inflate(activity, R.layout.logtrackable_without_geocode, null); builder.setView(layout); doNotAskAgain = (CheckBox) layout.findViewById(R.id.logtrackable_do_not_ask_me_again); final int showCount = Settings.getLogTrackableWithoutGeocodeShowCount(); Settings.setLogTrackableWithoutGeocodeShowCount(showCount + 1); builder.setPositiveButton(string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { checkDoNotAskAgain(); dialog.dismiss(); } }); builder.setNegativeButton(string.no, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { checkDoNotAskAgain(); dialog.dismiss(); // Post the log postLog(); } }); return builder.create(); } /** * Verify if doNotAskAgain is checked. * If true, set the counter to MAX_SHOWN_POPUP_TRACKABLE_WITHOUT_GEOCODE */ private void checkDoNotAskAgain() { if (doNotAskAgain.isChecked()) { Settings.setLogTrackableWithoutGeocodeShowCount(MAX_SHOWN_POPUP_TRACKABLE_WITHOUT_GEOCODE); } } } }