/*
* Copyright 2011 Matthew Precious
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mattprecious.locnotifier;
import java.util.List;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.location.Address;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockMapActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.mattprecious.locnotifier.RadiusOverlay.PointType;
import de.android1.overlaymanager.ManagedOverlay;
import de.android1.overlaymanager.ManagedOverlayGestureDetector;
import de.android1.overlaymanager.ManagedOverlayItem;
import de.android1.overlaymanager.OverlayManager;
import de.android1.overlaymanager.ZoomEvent;
public class ShowMap extends SherlockMapActivity {
public static final long MIN_DISTANCE = 50;
public static final String EXTRA_DEST_LAT = "dest_lat";
public static final String EXTRA_DEST_LNG = "dest_lng";
private LocationManager locationManager;
private LocationListener locationListener;
private SharedPreferences preferences;
private Vibrator vibrator;
private Location bestLocation;
private OverlayManager overlayManager;
private PointOverlay locationPoint;
private PointOverlay destinationPoint;
private RadiusOverlay locationRadius;
private RadiusOverlay destinationRadius;
private ManagedOverlay overlayListener;
private MapView mapView;
private MapController mapController;
private LinearLayout distanceBarPanel;
private SeekBar distanceBar;
private long distance;
private boolean gpsEnabled;
private boolean followLocation;
private List<Address> searchResults;
private final int DIALOG_ID_SEARCH = 1;
private final int DIALOG_ID_SEARCHING = 2;
private final int DIALOG_ID_SEARCH_RESULTS = 3;
@Override
protected void onCreate(Bundle icicle) {
// TODO Auto-generated method stub
super.onCreate(icicle);
setContentView(R.layout.map);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Bundle extras = getIntent().getExtras();
preferences = PreferenceManager.getDefaultSharedPreferences(this);
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
mapView = (MapView) findViewById(R.id.mapview);
mapController = mapView.getController();
distanceBarPanel = (LinearLayout) findViewById(R.id.distance_bar_panel);
distanceBar = (SeekBar) findViewById(R.id.distance_bar);
distanceBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
private UpdateDistanceTask updateDistanceTask;
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
seekBar.setProgress(4);
updateDistanceTask.cancel(true);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
updateDistanceTask = (UpdateDistanceTask) new UpdateDistanceTask().execute();
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
});
// TODO: Either change this preference type to a long or create a new preference
distance = (long) preferences.getFloat("dest_radius", MIN_DISTANCE);
gpsEnabled = preferences.getBoolean("use_gps", true);
followLocation = false;
overlayManager = new OverlayManager(this, mapView);
overlayListener = overlayManager.createOverlay("overlayListener");
overlayListener
.setOnOverlayGestureListener(new ManagedOverlayGestureDetector.OnOverlayGestureListener() {
@Override
public boolean onZoom(ZoomEvent zoom, ManagedOverlay overlay) {
stopFollow();
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e, ManagedOverlay overlay,
GeoPoint point, ManagedOverlayItem item) {
stopFollow();
mapController.animateTo(point);
mapController.zoomIn();
return true;
}
@Override
public void onLongPress(MotionEvent e, ManagedOverlay overlay) {
stopFollow();
// due to the weird behavior stated below, it's possible to have the user
// lift their finger up while the vibration is queued and waiting, so cancel
// any pending vibrations
vibrator.cancel();
// for some reason, longPressFinished won't fire until quite a while after
// longPress fires... so delay the vibration by 450ms
long[] pattern = { 450, 50, };
vibrator.vibrate(pattern, -1);
}
@Override
public void onLongPressFinished(MotionEvent e, ManagedOverlay overlay,
GeoPoint point, ManagedOverlayItem item) {
stopFollow();
showDestination(point);
}
@Override
public boolean onScrolled(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY, ManagedOverlay overlay) {
stopFollow();
return false;
}
@Override
public boolean onSingleTap(MotionEvent e, ManagedOverlay overlay,
GeoPoint point, ManagedOverlayItem item) {
stopFollow();
// due to the weird behavior stated above, it's possible to have the user
// lift their finger up while the vibration is queued and waiting, so cancel
// any pending vibrations
vibrator.cancel();
return false;
}
});
overlayManager.populate();
// Acquire a reference to the system Location Manager
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
// Define a listener that responds to location updates
locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
// Called when a new location is found by the network location provider.
if (LocationHelper.isBetterLocation(location, bestLocation)) {
bestLocation = location;
showLocation(location);
}
}
public void onStatusChanged(String provider, int status, Bundle extras) {
}
public void onProviderEnabled(String provider) {
}
public void onProviderDisabled(String provider) {
}
};
bestLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (bestLocation == null) {
bestLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
}
if (bestLocation != null) {
showLocation(bestLocation);
moveToLocation();
}
int dest_lat = preferences.getInt("dest_lat", 0);
int dest_lng = preferences.getInt("dest_lng", 0);
float dest_radius = preferences.getFloat("dest_radius", 0);
boolean moveToDestination = false;
if (extras != null && extras.containsKey(EXTRA_DEST_LAT)
&& extras.containsKey(EXTRA_DEST_LNG)) {
dest_lat = extras.getInt(EXTRA_DEST_LAT);
dest_lng = extras.getInt(EXTRA_DEST_LNG);
moveToDestination = true;
}
GeoPoint destination = new GeoPoint(dest_lat, dest_lng);
if (moveToDestination) {
mapController.animateTo(destination);
}
if (dest_lat != 0 && dest_lng != 0) {
destinationPoint = new PointOverlay(destination, PointType.DESTINATION);
}
if (dest_radius != 0) {
destinationRadius = new RadiusOverlay(destination, dest_radius, PointType.DESTINATION);
}
if (extras != null && extras.containsKey(Intent.EXTRA_TEXT)) {
String location = extras.getString(Intent.EXTRA_TEXT);
location = location.split("\n")[0];
search(location);
}
redraw();
showHint();
}
@Override
protected void onResume() {
super.onResume();
// Register the listener with the Location Manager to receive location updates
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0,
locationListener);
locationManager
.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, locationListener);
}
@Override
protected void onPause() {
super.onPause();
locationManager.removeUpdates(locationListener);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// this needs to be in here so that if the device is rotated while the dialog is open, a
// second one will be created over top of the existing one. Putting this in onDestroy does
// nothing for some reason...
removeDialog(DIALOG_ID_SEARCH_RESULTS);
}
@Override
protected void onDestroy() {
locationManager.removeUpdates(locationListener);
super.onDestroy();
}
@Override
protected boolean isRouteDisplayed() {
return false;
}
@Override
protected Dialog onCreateDialog(int id) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
Dialog dialog;
switch (id) {
case DIALOG_ID_SEARCH:
LinearLayout searchDialog = (LinearLayout) LayoutInflater.from(
getApplicationContext()).inflate(R.layout.search_dialog, null);
builder.setView(searchDialog);
builder.setTitle(R.string.search);
builder.setPositiveButton(R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
Dialog dialog = (Dialog) dialogInterface;
EditText queryText = (EditText) dialog
.findViewById(R.id.search_dialog_text);
search(queryText.getText().toString());
}
});
dialog = builder.create();
break;
case DIALOG_ID_SEARCHING:
dialog = ProgressDialog.show(this, null, getString(R.string.searching), true);
break;
case DIALOG_ID_SEARCH_RESULTS:
if (searchResults == null) {
return null;
}
String[] addresses = new String[searchResults.size()];
for (int i = 0; i < addresses.length; i++) {
addresses[i] = LocationHelper.addressToString(searchResults.get(i));
}
builder.setTitle(R.string.search_results);
builder.setCancelable(false);
builder.setNegativeButton(R.string.cancel_button,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
removeDialog(DIALOG_ID_SEARCH_RESULTS);
}
});
builder.setItems(addresses, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Address address = searchResults.get(which);
GeoPoint point = getPoint(address);
destinationPoint = new PointOverlay(point, PointType.DESTINATION);
redraw();
moveToDestination();
removeDialog(DIALOG_ID_SEARCH_RESULTS);
}
});
dialog = builder.create();
break;
default:
dialog = null;
}
return dialog;
}
@TargetApi(11)
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater = getSupportMenuInflater();
menuInflater.inflate(R.menu.map, menu);
// SearchView was added in HC
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setOnQueryTextListener(new OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
search(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
}
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.menu_gps_on).setVisible(gpsEnabled);
menu.findItem(R.id.menu_gps_off).setVisible(!gpsEnabled);
// TODO: Create a new icon for when we're currently following the user.
// Similar to the compass icon in GMM
// if (followLocation) {
// menu.findItem(R.id.menu_location).setIcon()
// }
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent intent = new Intent(this, LocationNotifier.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
return true;
case R.id.menu_save:
Editor editor = preferences.edit();
if (destinationPoint != null) {
GeoPoint destination = destinationPoint.getPoint();
editor.putInt("dest_lat", destination.getLatitudeE6());
editor.putInt("dest_lng", destination.getLongitudeE6());
}
editor.putFloat("dest_radius", distance);
editor.putBoolean("use_gps", gpsEnabled);
editor.commit();
if (LocationService.isRunning()) {
stopService(new Intent(getApplicationContext(), LocationService.class));
startService(new Intent(getApplicationContext(), LocationService.class));
}
finish();
return true;
case R.id.menu_location:
moveToLocation();
startFollow();
return true;
case R.id.menu_gps_on:
gpsEnabled = false;
Toast.makeText(getApplicationContext(), R.string.gps_off_toast, Toast.LENGTH_SHORT)
.show();
supportInvalidateOptionsMenu();
return true;
case R.id.menu_gps_off:
gpsEnabled = true;
Toast.makeText(getApplicationContext(), R.string.gps_on_toast, Toast.LENGTH_SHORT)
.show();
supportInvalidateOptionsMenu();
return true;
case R.id.menu_distance:
if (destinationPoint == null) {
Toast.makeText(getApplicationContext(), R.string.no_destination,
Toast.LENGTH_SHORT).show();
return true;
}
stopFollow();
if (distanceBarPanel.getVisibility() == View.GONE) {
distanceBarPanel.setVisibility(View.VISIBLE);
moveToDestination();
} else {
distanceBarPanel.setVisibility(View.GONE);
}
return true;
case R.id.menu_search:
// only need this for pre-HC
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
showDialog(DIALOG_ID_SEARCH);
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void showHint() {
int hintShown = preferences.getInt("hint_shown", 0);
if (hintShown < 5) {
Toast hintToast = Toast.makeText(this, R.string.hint_toast, Toast.LENGTH_LONG);
hintToast.show();
Editor editor = preferences.edit();
editor.putInt("hint_shown", hintShown + 1);
editor.commit();
}
}
private void search(String query) {
new SearchTask().execute(query);
}
private void moveToLocation() {
if (bestLocation != null) {
moveTo(getPoint(bestLocation));
}
}
private void moveToDestination() {
if (destinationPoint != null) {
moveTo(destinationPoint.getPoint());
}
}
private void moveTo(GeoPoint point) {
if (point != null) {
mapController.animateTo(point);
if (mapView.getZoomLevel() < 17) {
mapController.setZoom(17);
}
}
}
private void startFollow() {
followLocation = true;
supportInvalidateOptionsMenu();
}
private void stopFollow() {
// Only do this when followLocation is true.
// If the search field is active, invalidating the menu will cause the keyboard to pop open
// again. It's really annoying.
if (followLocation) {
followLocation = false;
supportInvalidateOptionsMenu();
}
}
private void showLocation(Location location) {
GeoPoint point = getPoint(location);
locationPoint = new PointOverlay(point, PointType.LOCATION);
locationRadius = new RadiusOverlay(point, location.getAccuracy(), PointType.LOCATION);
redraw();
if (followLocation) {
moveToLocation();
}
}
private void showDestination(GeoPoint point) {
destinationPoint = new PointOverlay(point, PointType.DESTINATION);
destinationRadius = new RadiusOverlay(point, distance, PointType.DESTINATION);
redraw();
}
private void redraw() {
List<Overlay> mapOverlays = mapView.getOverlays();
mapOverlays.clear();
if (locationRadius != null) {
mapOverlays.add(locationRadius);
}
if (locationPoint != null) {
mapOverlays.add(locationPoint);
}
if (destinationRadius != null) {
mapOverlays.add(destinationRadius);
}
if (destinationPoint != null) {
mapOverlays.add(destinationPoint);
}
overlayManager.populate();
mapView.invalidate();
}
private GeoPoint getPoint(Location location) {
Double lat = location.getLatitude() * 1E6;
Double lng = location.getLongitude() * 1E6;
return new GeoPoint(lat.intValue(), lng.intValue());
}
private GeoPoint getPoint(Address address) {
Double lat = address.getLatitude() * 1E6;
Double lng = address.getLongitude() * 1E6;
return new GeoPoint(lat.intValue(), lng.intValue());
}
public class SearchTask extends AsyncTask<String, Void, List<Address>> {
@Override
protected void onPreExecute() {
showDialog(DIALOG_ID_SEARCHING);
}
@Override
public List<Address> doInBackground(String... query) {
return LocationHelper.stringToAddresses(getApplicationContext(), query[0]);
}
@Override
protected void onPostExecute(List<Address> result) {
searchResults = result;
dismissDialog(DIALOG_ID_SEARCHING);
if (searchResults != null && searchResults.size() > 0) {
showDialog(DIALOG_ID_SEARCH_RESULTS);
} else {
Toast.makeText(getApplicationContext(), R.string.no_results, Toast.LENGTH_SHORT)
.show();
}
}
}
public class UpdateDistanceTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
while (!this.isCancelled()) {
// progress bar has a size of 9, so 4 is the midpoint
// don't do anything in the middle
if (distanceBar.getProgress() == 4) {
continue;
}
// get absolute difference
long modifier = Math.abs(distanceBar.getProgress() - 4);
// adjust to exponential growth
modifier = (long) Math.pow(2, modifier);
// bring in negation
modifier = distanceBar.getProgress() < 4 ? -modifier : modifier;
// adjust based on zoom level
// zoom level increases as you zoom in, we want to flip this around so the more
// zoomed in you are, the less impact it has on the distance slider
int invertedZoom = mapView.getMaxZoomLevel() - mapView.getZoomLevel() + 1;
modifier = (long) (modifier * Math.pow(2, invertedZoom - 3));
distance = Math.max(distance + modifier, MIN_DISTANCE);
destinationRadius = new RadiusOverlay(destinationPoint.getPoint(), distance,
PointType.DESTINATION);
mapView.post(new Runnable() {
@Override
public void run() {
redraw();
}
});
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// do nothing
}
}
return null;
}
}
}