package de.blau.android;
import java.util.List;
import org.acra.ACRA;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RadioGroup.OnCheckedChangeListener;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Spinner;
import android.widget.TextView;
import de.blau.android.dialogs.ErrorAlert;
import de.blau.android.exception.OsmException;
import de.blau.android.osm.BoundingBox;
import de.blau.android.prefs.AdvancedPrefDatabase;
import de.blau.android.prefs.AdvancedPrefDatabase.Geocoder;
import de.blau.android.prefs.Preferences;
import de.blau.android.util.BugFixedAppCompatActivity;
import de.blau.android.util.GeoMath;
import de.blau.android.util.Search;
import de.blau.android.util.Search.SearchResult;
/**
* Activity where the user can pick a Location and a radius (more precisely: a
* square with "radius" as half of the edge length. This class will return
* valid geo boundaries for a {@link BoundingBox} as extra data. ResultType
* will be RESULT_OK when the {@link BoundingBox} should be loaded from a OSM
* Server, otherwise RESULT_CANCEL.<br>
* This class acts as its own LocationListener: We will offers the best
* location to the user.
*
* @author mb
*/
public class BoxPicker extends BugFixedAppCompatActivity implements LocationListener {
/**
* Tag used for Android-logging.
*/
private final static String DEBUG_TAG = BoxPicker.class.getName();
/**
* LocationManager. Needed as field for unregister in {@link #onPause()}.
*/
private LocationManager locationManager = null;
/**
* The current location with the best accuracy.
*/
private Location currentLocation = null;
/**
* The user-chosen radius by the SeekBar. Value in Meters.
*/
private int currentRadius = 0;
/**
* Last known location.
*/
private Location lastLocation = null;
/**
* Tag for Intent extras.
*/
public static final String RESULT_LEFT = "de.blau.android.BoxPicker.left";
/**
* Tag for Intent extras.
*/
public static final String RESULT_BOTTOM = "de.blau.android.BoxPicker.bottom";
/**
* Tag for Intent extras.
*/
public static final String RESULT_RIGHT = "de.blau.android.BoxPicker.right";
/**
* Tag for Intent extras.
*/
public static final String RESULT_TOP = "de.blau.android.BoxPicker.top";
private static final int MIN_WIDTH = 50;
/**
* Registers some listeners, sets the content view and initialize
* {@link #currentRadius}.</br> {@inheritDoc}
*/
@Override
protected void onCreate(final Bundle savedInstanceState) {
final Preferences prefs = new Preferences(this);
if (prefs.lightThemeEnabled()) {
setTheme(R.style.Theme_customLight);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.location_picker_view);
//Load Views
RadioGroup radioGroup = (RadioGroup) findViewById(R.id.location_type_group);
final Button loadMapButton = (Button) findViewById(R.id.location_button_current);
Button dontLoadMapButton = ((Button) findViewById(R.id.location_button_no_location));
final EditText latEdit = (EditText) findViewById(R.id.location_lat_edit);
final EditText lonEdit = (EditText) findViewById(R.id.location_lon_edit);
EditText searchEdit = (EditText) findViewById(R.id.location_search_edit);
SeekBar seeker = (SeekBar) findViewById(R.id.location_radius_seeker);
currentRadius = seeker.getProgress();
//register listeners
seeker.setOnSeekBarChangeListener(createSeekBarListener());
radioGroup.setOnCheckedChangeListener(createRadioGroupListener(loadMapButton, null /* dontLoadMapButton */, latEdit,
lonEdit));
OnClickListener onClickListener = createButtonListener(radioGroup, latEdit, lonEdit);
loadMapButton.setOnClickListener(onClickListener);
dontLoadMapButton.setOnClickListener(onClickListener);
final Spinner searchGeocoder = (Spinner) findViewById(R.id.location_search_geocoder);
AdvancedPrefDatabase db = new AdvancedPrefDatabase(this);
final Geocoder[] geocoders = db.getActiveGeocoders();
String[] geocoderNames = new String[geocoders.length];
for (int i=0;i<geocoders.length;i++) {
geocoderNames[i] = geocoders[i].name;
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
this, android.R.layout.simple_spinner_item, geocoderNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
searchGeocoder.setAdapter(adapter);
searchGeocoder.setSelection(prefs.getGeocoder());
searchGeocoder.setOnItemSelectedListener(new OnItemSelectedListener(){
@Override
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long arg3) {
prefs.setGeocoder(pos);
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
}});
final de.blau.android.util.SearchItemFoundCallback searchItemFoundCallback = new de.blau.android.util.SearchItemFoundCallback() {
private static final long serialVersionUID = 1L;
@Override
public void onItemFound(SearchResult sr) {
RadioButton rb = (RadioButton) findViewById(R.id.location_coordinates);
rb.setChecked(true); // note potential race condition with setting the lat/lon
LinearLayout coordinateView = (LinearLayout) findViewById(R.id.location_coordinates_layout);
coordinateView.setVisibility(View.VISIBLE);
loadMapButton.setEnabled(true);
latEdit.setText(Double.toString(sr.getLat()));
lonEdit.setText(Double.toString(sr.getLon()));
}
};
searchEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH
|| (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
Search search = new Search(BoxPicker.this, searchItemFoundCallback);
search.find(geocoders[searchGeocoder.getSelectedItemPosition()],v.getText().toString(),null);
return true;
}
return false;
}
});
ActionBar actionbar = getSupportActionBar();
actionbar.setDisplayHomeAsUpEnabled(true);
}
@Override
protected void onPause() {
try {
locationManager.removeUpdates(this);
} catch (SecurityException sex) {
// can be safely ignored
}
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
Location l = registerLocationListener();
if (l != null) {
lastLocation = l;
}
setLocationRadioButton(R.id.location_last, R.string.location_last_text_parameterized, lastLocation, null);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Registers this class for location updates from all available location
* providers.
*/
private Location registerLocationListener() {
Preferences prefs = new Preferences(this);
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
List<String> providers = locationManager.getProviders(true);
Location bestLocation = null;
for (String provider : providers) {
try {
locationManager.requestLocationUpdates(provider,
prefs.getGpsInterval(), prefs.getGpsDistance(), this);
Location location = locationManager.getLastKnownLocation(provider);
if (bestLocation == null || !bestLocation.hasAccuracy() ||
(location != null && location.hasAccuracy() &&
location.getAccuracy() < bestLocation.getAccuracy())) {
bestLocation = location;
}
} catch (IllegalArgumentException e) {
} catch (SecurityException e) {
}
}
return bestLocation;
}
/**
* As soon as the user checks one of the radio buttons, the "load/don't
* load"-buttons will be enabled. Additionally, the lat/lon-EditTexts will
* be visible/invisible when the user chooses to insert the coordinate
* manually.
*
* @param loadMapButton the "Load!"-button.
* @param dontLoadMapButton the "Don't load anything"-button.
* @param latEdit latitude EditText.
* @param lonEdit longitude EditText.
* @return the new created listener.
*/
private OnCheckedChangeListener createRadioGroupListener(final Button loadMapButton,
final Button dontLoadMapButton, final EditText latEdit, final EditText lonEdit) {
return new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(final RadioGroup group, final int checkedId) {
LinearLayout coordinateView = (LinearLayout) findViewById(R.id.location_coordinates_layout);
loadMapButton.setEnabled(true);
// dontLoadMapButton.setEnabled(true);
if (checkedId == R.id.location_coordinates) {
coordinateView.setVisibility(View.VISIBLE);
// don't overwrite existing values...
if (latEdit.getText().length() == 0 && lonEdit.getText().length() == 0) {
if (currentLocation != null) {
latEdit.setText(Double.toString(currentLocation.getLatitude()));
lonEdit.setText(Double.toString(currentLocation.getLongitude()));
} else if (lastLocation != null) {
latEdit.setText(Double.toString(lastLocation.getLatitude()));
lonEdit.setText(Double.toString(lastLocation.getLongitude()));
}
}
} else {
coordinateView.setVisibility(View.GONE);
}
}
};
}
/**
* First, the minimum radius will be assured, second, the
* {@link #currentRadius} will be set and the label will be updated.
*
* @return the new created listener.
*/
private OnSeekBarChangeListener createSeekBarListener() {
return new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(final SeekBar seekBar, int progress, final boolean fromTouch) {
if (progress < MIN_WIDTH) {
progress = MIN_WIDTH;
}
currentRadius = progress;
TextView radiusText = (TextView) findViewById(R.id.location_radius_text);
radiusText.setText(" " + progress + "m");
}
@Override
public void onStartTrackingTouch(final SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(final SeekBar arg0) {
}
};
}
/**
* Reads the manual coordinate EditTexts and registers the button
* listeners.
*
* @param radioGroup
* @param latEdit Manual Latitude EditText.
* @param lonEdit Manual Longitude EditText.
*/
private OnClickListener createButtonListener(final RadioGroup radioGroup, final EditText latEdit,
final EditText lonEdit) {
return new OnClickListener() {
@Override
public void onClick(final View view) {
String lat = latEdit.getText().toString();
String lon = lonEdit.getText().toString();
performClick(view.getId(), radioGroup.getCheckedRadioButtonId(), lat, lon);
}
};
}
/**
* Do the action when the user clicks a Button. Generates the
* {@link BoundingBox} from the coordinate and chosen radius, sets the
* resultType (RESULT_OK when a map should be loaded, otherwise false) and
* calls {@link #sendResultAndExit(BoundingBox, int)}
*
* @param buttonId android-id from the clicked Button.
* @param checkedRadioButtonId android-id from the checked RadioButton.
* @param lat latitude from the EditText.
* @param lon longitude from the EditText.
*/
private void performClick(final int buttonId, final int checkedRadioButtonId, final String lat, final String lon) {
BoundingBox box = null;
int resultState = (buttonId == R.id.location_button_current) ? RESULT_OK : RESULT_CANCELED;
// return bbox even if cancelled
// if (resultState == RESULT_CANCELED)
// finish();
switch (checkedRadioButtonId) {
case R.id.location_current:
box = createBoxForCurrentLocation();
break;
case R.id.location_last:
box = createBoxForLastLocation();
break;
case R.id.location_coordinates:
box = createBoxForManualLocation(lat, lon);
break;
}
if (box != null) {
sendResultAndExit(box, resultState);
} else {
finish();
}
}
/**
* @return {@link BoundingBox} for {@link #currentLocation} and
* {@link #currentRadius}
*/
private BoundingBox createBoxForCurrentLocation() {
BoundingBox box = null;
try {
box = GeoMath.createBoundingBoxForCoordinates(currentLocation.getLatitude(),
currentLocation.getLongitude(), currentRadius, true);
} catch (OsmException e) {
ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH");
ACRA.getErrorReporter().handleException(e);
}
return box;
}
/**
* @return {@link BoundingBox} for {@link #lastLocation} and
* {@link #currentRadius}
*/
private BoundingBox createBoxForLastLocation() {
BoundingBox box = null;
try {
box = GeoMath.createBoundingBoxForCoordinates(lastLocation.getLatitude(),
lastLocation.getLongitude(), currentRadius, true);
} catch (OsmException e) {
ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH");
ACRA.getErrorReporter().handleException(e);
}
return box;
}
/**
* Tries to parse lat and lon and creates a new {@link BoundingBox} if
* successful.
*
* @param lat manual latitude
* @param lon manual longitude
* @return {@link BoundingBox} for lat, lon and {@link #currentRadius}
*/
private BoundingBox createBoxForManualLocation(final String lat, final String lon) {
BoundingBox box = null;
try {
float userLat = Float.parseFloat(lat);
float userLon = Float.parseFloat(lon);
box = GeoMath.createBoundingBoxForCoordinates(userLat, userLon, currentRadius, true);
} catch (NumberFormatException e) {
ErrorAlert.showDialog(this, ErrorCodes.NAN);
} catch (OsmException e) {
ErrorAlert.showDialog(this, ErrorCodes.NAN);
}
return box;
}
/**
* Creates the {@link Intent} with the boundaries of box as extra data.
*
* @param box the box with the chosen boundaries.
* @param resultState RESULT_OK when the map should be loaded, otherwise
* RESULT_CANCEL.
*/
private void sendResultAndExit(final BoundingBox box, final int resultState) {
Intent intent = new Intent();
intent.putExtra(RESULT_LEFT, box.getLeft());
intent.putExtra(RESULT_BOTTOM, box.getBottom());
intent.putExtra(RESULT_RIGHT, box.getRight());
intent.putExtra(RESULT_TOP, box.getTop());
setResult(resultState, intent);
finish();
}
/**
* When a location was found which has more accuracy than
* {@link #currentLocation}, then the newLocation will be set as
* currentLocation.<br>
* {@inheritDoc}
*/
@Override
public void onLocationChanged(final Location newLocation) {
Log.w(DEBUG_TAG, "Got location: " + newLocation);
if (newLocation != null) {
if (isNewLocationMoreAccurate(newLocation)) {
setLocationRadioButton(R.id.location_current, R.string.location_current_text_parameterized, newLocation, null);
currentLocation = newLocation;
if (lastLocation != null) {
setLocationRadioButton(R.id.location_last, R.string.location_last_text_parameterized, newLocation, lastLocation);
}
}
}
}
/**
* Set the text of a location (last or current) radio button.
* @param buttonId The resource ID of the radio button.
* @param textId The resource ID of the button text.
* @param location The location data to update the button text (may be null).
* @param lastLocation TODO
*/
private void setLocationRadioButton(final int buttonId, final int textId, final Location location, Location lastLocation) {
String locationMetaData = getString(R.string.location_text_unknown);
if (location != null) {
String accuracyMetaData = "";
double lat = location.getLatitude();
double lon = location.getLongitude();
accuracyMetaData = " (";
if (location.hasAccuracy()) {
accuracyMetaData += getString(R.string.location_text_metadata_accuracy, location.getAccuracy());
}
accuracyMetaData += " " + location.getProvider() + ")";
long fixTime = Math.max(0,(System.currentTimeMillis()-location.getTime())/1000);
//TODO slightly hackish, should be localized correctly
String fixString = "";
if (fixTime > 24*3600) {
fixString = fixTime / (24*3600) + " " + getString(R.string.days);
} else if (fixTime > 3600) {
fixString = fixTime / 3600 + " " + getString(R.string.hours);
} else if (fixTime > 60) {
fixString = fixTime / 60 + " " + getString(R.string.minutes);
} else {
fixString = fixTime + " " + getString(R.string.seconds);
}
if (lastLocation != null && currentLocation != null) { // add distance from old location
int fixDistance = Math.round(lastLocation.distanceTo(currentLocation));
if (fixDistance > 1000) {
fixString = Math.round(fixDistance/1000.0) + " " + getString(R.string.km) + " " + fixString;
} else {
fixString = fixDistance + " " + getString(R.string.meter) + " " + fixString;
}
}
locationMetaData = getString(R.string.location_text_metadata_location, lat, lon, accuracyMetaData, fixString);
}
RadioButton rb = (RadioButton)findViewById(buttonId);
rb.setEnabled(location != null);
rb.setText(getString(textId, locationMetaData));
}
/**
* Checks if the new location is more accurate than
* {@link #currentLocation}.
*
* @param newLocation new location
* @return true, if the new location is more accurate than the old one or
* one of them has no accuracy anyway.
*/
private boolean isNewLocationMoreAccurate(final Location newLocation) {
return currentLocation == null || !newLocation.hasAccuracy() || !currentLocation.hasAccuracy()
|| newLocation.getAccuracy() <= currentLocation.getAccuracy();
}
/**
* {@inheritDoc}
*/
@Override
public void onProviderDisabled(final String provider) {
}
/**
* {@inheritDoc}
*/
@Override
public void onProviderEnabled(final String provider) {
}
/**
* {@inheritDoc}
*/
@Override
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
}
}