/*
* GeoSolutions map - Digital field mapping on Android based devices
* Copyright (C) 2013 GeoSolutions (www.geo-solutions.it)
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package it.geosolutions.android.map.control;
import it.geosolutions.android.map.R;
import it.geosolutions.android.map.view.AdvancedMapView;
import org.mapsforge.android.maps.MapView;
import org.mapsforge.android.maps.overlay.Marker;
import org.mapsforge.android.maps.overlay.MyLocationOverlay;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.widget.ImageButton;
import android.widget.Toast;
/**
* Control that manage GeoLocation overlay.
* Add and remove the <MyLocationOverlay> on the map.
* Allow controlling snap to location, using long click.
*
* NOTE: This control does not receives gps position events
*
* @author Lorenzo Natali (www.geo-solutions.it)
*/
public class LocationControl extends MapControl implements OnLongClickListener, OnTouchListener {
// MapsForge Overlay to display device position
MyLocationOverlay overlay = null;
boolean centerAtFirst = false;
/** flag to disable messages **/
boolean restoring = false;
/** preserve the previous status to choose message and icon change */
int previousStatus = 0;
private static final int MESSAGE_UNAVAILABLE = R.string.location_unavailable_message;
private static final int MESSAGE_DEACTIVATED = R.string.location_deactivated_message;
private static final int MESSAGE_ACTIVATED = R.string.location_activated_message;
private static final int MESSAGE_SNAP_ACTIVATED = R.string.location_snap_activated_message;
private static final int MESSAGE_SNAP_DEACTIVATED = R.string.location_snap_deactivated_message;
private static final int MESSAGE_PROMPT_POSITION_ACCESS = R.string.location_promt_position_permission;
private static final int MESSAGE_NETWORK_LOCATION_AVAILABLE = R.string.location_network_available_message;
private static final String DEFAULT_CONTROL_ID = "LOCATION_CONTROL_STATE";
private static final String ENABLED = "ENABLED";
private static final String SNAP = "SNAP";
/**
* Location provider status
*/
private int locationProviderStatus = LocationProvider.OUT_OF_SERVICE;
public Location lastLocation;
/**
* Animation for the fixing gps icon
*/
AnimationDrawable animation;
/**
* @param view
*/
public LocationControl(AdvancedMapView view) {
super(view);
animation = new AnimationDrawable();
animation.addFrame(view.getResources().getDrawable(R.drawable.ic_device_access_location_searching), 500);
animation.addFrame(view.getResources().getDrawable(R.drawable.ic_device_access_location_found), 500);
animation.setOneShot(false);
}
/**
* Called when control status changes
*/
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
Log.v("Location", "Setting to "+enabled);
if (enabled) {
// check if GPS is activated
initMyLocationOverlay();
//addOverlay();
locationProviderStatus=LocationProvider.TEMPORARILY_UNAVAILABLE;
checkLocationEnabled(true);
if(lastLocation != null){
overlay.onLocationChanged(lastLocation);
}
refreshIcon();
} else {
if(overlay != null){
overlay.disableMyLocation();
//removeOverlay();
setSnap(false);
int currentStatus = MESSAGE_DEACTIVATED;
sendMessageIfNeeded(currentStatus);
// refresh icon
refreshIcon();
}
}
activationButton.setSelected(false);
}
/**
* remove location overlay from the map
*/
public void removeOverlay() {
view.overlayManger.removeOverlay(overlay);
}
/**
* initialize the overlay. Add to the map if not present. Overrides the
* <MyLocationOverlay> methods to change icon when the location provider is
* unavailable or they are manually disabled.
*/
private void initMyLocationOverlay() {
if (overlay == null) {
// create the icon
Drawable drawable = view.getResources().getDrawable(
R.drawable.ic_maps_indicator_current_position_anim1);
drawable = Marker.boundCenter(drawable);
overlay = new MyLocationOverlay(view.getContext(), (MapView) view,
drawable, getDefaultCircleFill(), getDefaultCircleStroke()) {
/*
* TODO: JAVADOC (non-Javadoc)
* @see org.mapsforge.android.maps.overlay.MyLocationOverlay#onLocationChanged(android.location.Location)
*/
@Override
public void onLocationChanged(Location location) {
try{
super.onLocationChanged(location);
lastLocation = location;
Log.v("LOCATION","Location changed, setting AVAILABLE");
locationProviderStatus = LocationProvider.AVAILABLE;
refreshIcon();
}catch(IllegalStateException e){
//avoid exception and centering the map
Log.w("LOCATION","problem during map redraw");
}
}
/*
* TODO: JAVADOC (non-Javadoc)
* @see org.mapsforge.android.maps.overlay.MyLocationOverlay#onLocationChanged(android.location.Location)
*/
@Override
public void onProviderDisabled(String provider) {
super.onProviderDisabled(provider);
if (isEnabled()) {
Log.v("LOCATION", "provider disabled: " + provider);
enableMyLocation(false);
// TODO: better management of this short lived object
LocationManager locationManager = (LocationManager) view.getContext().getSystemService(Activity.LOCATION_SERVICE);
if ( !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
&& !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationProviderStatus = LocationProvider.OUT_OF_SERVICE;
} else{
// TODO check confidence
locationProviderStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
}
refreshStatus();
}
}
/*
* TODO: JAVADOC (non-Javadoc)
* @see org.mapsforge.android.maps.overlay.MyLocationOverlay#onLocationChanged(android.location.Location)
*/
@Override
public void onProviderEnabled(String provider) {
super.onProviderEnabled(provider);
if (isEnabled()) {
enableMyLocation(false);
enableMyLocation(true);
locationProviderStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
Log.v("LOCATION", "provider enabled: " + provider);
refreshStatus();
}
}
/*
* TODO: JAVADOC (non-Javadoc)
* @see org.mapsforge.android.maps.overlay.MyLocationOverlay#onLocationChanged(android.location.Location)
*/
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
super.onStatusChanged(provider, status, extras);
if (isEnabled()) {
enableMyLocation(false);
Log.v("LOCATION","Status changed, setting "+status);
locationProviderStatus = status;
refreshStatus();
}
}
};
// If overlay is correctly created, add to the map
addOverlay();
}
}
/**
* Add the overlay to the top of layers
*/
private void addOverlay() {
view.getOverlayManger().addLocationOverlay(overlay);
};
/*
* (non-Javadoc)
* @see
* it.geosolutions.android.map.control.MapControl#draw(android.graphics.Canvas)
*/
@Override
public void draw(Canvas canvas) {
// TODO Auto-generated method stub
}
@Override
public void setActivationButton(ImageButton imageButton) {
super.setActivationButton(imageButton);
super.setMapListener(this);
imageButton.setLongClickable(true);
imageButton.setOnLongClickListener(this);
}
/*
* (non-Javadoc)
* @see android.view.View.OnLongClickListener#onLongClick(android.view.View)
*/
@Override
public boolean onLongClick(View view) {
// if the control is not enable, enable it and set snapToLocation
// TODO: higlight button as pressed
if (isEnabled() && overlay.isMyLocationEnabled()) {
toggleSnap();
int currentStatus = overlay.isSnapToLocationEnabled() ? MESSAGE_SNAP_ACTIVATED
: MESSAGE_SNAP_DEACTIVATED;
sendMessageIfNeeded(currentStatus);
activationButton.setSelected(true);
refreshIcon();
return true;
}
activationButton.setSelected(false);
return false;
}
/**
* Toggle the snap to location option
*/
private void toggleSnap() {
setSnap(!overlay.isSnapToLocationEnabled());
}
/**
* set snap to location and changes icon of the button
*/
private void setSnap(boolean snap) {
if(overlay!=null){
overlay.setSnapToLocationEnabled(snap);
refreshIcon();
}
}
/**
* changes the icon checking the snap mode and the location service availability
* returns an icon with the new status
*/
private int refreshIcon() {
int currentStatus = previousStatus;
if (!isEnabled()) {
// Location is turned off
currentStatus = MESSAGE_DEACTIVATED;
animation.stop();
activationButton
.setImageResource(R.drawable.ic_device_access_location_searching);
} else if (overlay == null) {
// Location is turned on but no overlay is found
Log.w("LOCATION", "Location is turned on but no overlay is found");
currentStatus = 0;
animation.stop();
activationButton.setImageResource(R.drawable.ic_device_access_location_off);
} else if (overlay.isMyLocationEnabled()) {
// Check LocationProvider status
switch (locationProviderStatus) {
case LocationProvider.OUT_OF_SERVICE:
animation.stop();
activationButton.setImageResource(R.drawable.ic_device_access_location_off);
break;
case LocationProvider.TEMPORARILY_UNAVAILABLE:
Log.v("LOCATION","This should be blinking now");
activationButton.setImageDrawable(animation);
animation.start();
break;
case LocationProvider.AVAILABLE:
animation.stop();
activationButton.setImageResource(R.drawable.ic_device_access_location_found);
break;
default:
animation.stop();
Log.w("LOCATION", "LocationProviderStatus is "+locationProviderStatus+" but value does not means anything");
break;
}
// my location unavailable
} else {
currentStatus = MESSAGE_UNAVAILABLE;
Log.v("LOCATION","MESSAGE_UNAVAILABLE");
activationButton.setImageResource(R.drawable.ic_device_access_location_off);
}
/*
* Why a view method act as a control method?
if(isEnabled()){
activationButton.setSelected(true);
}else{
activationButton.setSelected(false);
}
*/
activationButton.setSelected(overlay.isSnapToLocationEnabled());
return currentStatus;
}
/**
* @return a default Paint for the location precision circle
*/
private static Paint getDefaultCircleFill() {
return getPaint(Style.FILL, Color.BLUE, 30);
}
/**
* @return a default Stroke for the location precision circle
*/
private static Paint getDefaultCircleStroke() {
Paint paint = getPaint(Style.STROKE, Color.BLUE, 128);
paint.setStrokeWidth(2);
return paint;
}
/**
* creates a <Paint> using a <Style>, a color and the trasperency
*
* @param style the Paint style
* @param color the color
* @param alpha the trasparency index
* @return
*/
private static Paint getPaint(Style style, int color, int alpha) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(style);
paint.setColor(color);
paint.setAlpha(alpha);
return paint;
}
/**
* Check if Gps is enabled. If it is not, a dialog suggests to enable it. if the
* GPS is available, simply activate the location and shows a <Toast> message
* about the activation
* @param b
*/
private void checkLocationEnabled(boolean enable) {
LocationManager locationManager = (LocationManager) view.getContext().getSystemService(Activity.LOCATION_SERVICE);
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) &&
!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
buildAlertMessageNoLocation(enable);
} else {
// if a location provider is enabled, activate the location overlay
if(enable){
overlay.enableMyLocation(centerAtFirst);
}
if (overlay.isMyLocationEnabled()) {
// default snap on click
setSnap(true);
int currentStatus = overlay.isSnapToLocationEnabled() ? MESSAGE_SNAP_ACTIVATED : MESSAGE_ACTIVATED;
sendMessageIfNeeded(currentStatus);
}
}
return;
}
/**
* Create a dialog to ask the user if he wants to activate a location provider.
* If the user agree, the control is disabled and the location source
* setting window of the system is opened
* If no provider is available, the tool will be disabled
* @param enable
*/
private void buildAlertMessageNoLocation(final boolean enable) {
if(restoring){
overlay.enableMyLocation(enable);
return;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
builder.setMessage(MESSAGE_PROMPT_POSITION_ACCESS)
.setCancelable(false)
// the positive button event
.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog,
final int id) {
Context c = view.getContext();
// TODO: use startActivityForResult
// Can't use startactivityforResult because this is not an activity
c.startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
overlay.enableMyLocation(true);
// activationButton.setSelected(false);
//refreshIcon();
}
})
// the negative button event
.setNegativeButton(android.R.string.no,
new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
if(enable){
overlay.enableMyLocation(true);
}
// TODO: This part is not needed
// TODO: better management of this short lived object
LocationManager locationManager = (LocationManager) view.getContext().getSystemService(Activity.LOCATION_SERVICE);
if (!locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationProviderStatus = LocationProvider.OUT_OF_SERVICE;
sendMessageIfNeeded(MESSAGE_UNAVAILABLE);
// TODO: disable button?
} else{
locationProviderStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
if (!overlay.isMyLocationEnabled()) {
sendMessageIfNeeded(MESSAGE_UNAVAILABLE);
} else {
int currentStatus = overlay.isSnapToLocationEnabled() ? MESSAGE_SNAP_ACTIVATED : MESSAGE_NETWORK_LOCATION_AVAILABLE;
sendMessageIfNeeded(currentStatus);
}
}
refreshIcon();
}
});
final AlertDialog alert = builder.create();
alert.show();
}
/**
* refresh the icon and show a message if needed
*/
public void refreshStatus() {
sendMessageIfNeeded(refreshIcon());
}
/**
* show the message if the status changed and it is useful
*
* @param changeIcon
*/
private void sendMessageIfNeeded(int status) {
if(restoring) return;
if (previousStatus == status) {
return;
}
if (isEnabled()) {
Toast.makeText(view.getContext(), status, Toast.LENGTH_LONG).show();
} else if (status == MESSAGE_DEACTIVATED) {
Toast.makeText(view.getContext(), MESSAGE_DEACTIVATED,
Toast.LENGTH_LONG).show();
}
previousStatus = status;
}
// SAVE AND RESTORE STATE OF THE CONTROL
/**
* Add a bundle with the control state by id
* supposes to use the same id on restore
*/
@Override
public void saveState(Bundle savedInstanceState) {
super.saveState(savedInstanceState);
String id = controlId !=null ? controlId : DEFAULT_CONTROL_ID;
Bundle b = new Bundle();
b.putBoolean(ENABLED, isEnabled());
if(overlay !=null){
b.putBoolean(SNAP,overlay.isSnapToLocationEnabled());
}
savedInstanceState.putBundle(id, b);
Log.d("LOCATION", "State saved");
}
@Override
public void restoreState(Bundle savedInstanceState) {
super.restoreState(savedInstanceState);
Log.d("LOCATION", "State restored");
if(savedInstanceState==null){
return;
}
String id = controlId !=null ? controlId : DEFAULT_CONTROL_ID;
Bundle b = savedInstanceState.getBundle(id);
if(b!=null){
restoring = true;
setEnabled(b.getBoolean(ENABLED,false));
setSnap(b.getBoolean(SNAP,false));
restoring = false;
}
}
@Override
public void refreshControl(int requestCode, int resultCode, Intent data) {
Log.d("LOCATION", "Resfreshing control");
// I cannot use the result of the "enable gps" activity
// so I re-set the icon each time the control is refreshed
if(isEnabled()){
LocationManager locationManager = (LocationManager) view.getContext().getSystemService(Activity.LOCATION_SERVICE);
if ( !locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
&& !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationProviderStatus = LocationProvider.OUT_OF_SERVICE;
} else{
// TODO check confidence
locationProviderStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
}
refreshStatus();
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// Disable the button on map move
if(event.getAction() == MotionEvent.ACTION_MOVE){
if(overlay.isSnapToLocationEnabled()){
// reset Toast message
previousStatus = 0;
setSnap(false);
refreshStatus();
}
}
return false;
}
}