package de.blau.android; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.MalformedURLException; import java.net.ProtocolException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import org.acra.ACRA; import android.Manifest; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.drawable.StateListDrawable; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.location.Location; import android.location.LocationManager; import android.net.ConnectivityManager; import android.net.Uri; import android.nfc.Tag; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.provider.MediaStore; import android.provider.Settings; import android.speech.RecognizerIntent; import android.support.annotation.NonNull; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.view.ActionMode; import android.support.v7.widget.ActionMenuView; import android.support.v7.widget.Toolbar; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextThemeWrapper; import android.view.InputDevice; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnCreateContextMenuListener; import android.view.View.OnGenericMotionListener; import android.view.View.OnKeyListener; import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.RelativeLayout.LayoutParams; import de.blau.android.GeoUrlActivity.GeoUrlData; import de.blau.android.Logic.CursorPaddirection; import de.blau.android.RemoteControlUrlActivity.RemoteControlUrlData; import de.blau.android.actionbar.UndoDialogFactory; import de.blau.android.contract.Paths; import de.blau.android.dialogs.BackgroundProperties; import de.blau.android.dialogs.ConfirmUpload; import de.blau.android.dialogs.DataLossActivity; import de.blau.android.dialogs.DownloadCurrentWithChanges; import de.blau.android.dialogs.ElementInfo; import de.blau.android.dialogs.ErrorAlert; import de.blau.android.dialogs.GpxUpload; import de.blau.android.dialogs.ImportTrack; import de.blau.android.dialogs.NewVersion; import de.blau.android.dialogs.Newbie; import de.blau.android.dialogs.Progress; import de.blau.android.dialogs.SearchForm; import de.blau.android.easyedit.EasyEditManager; import de.blau.android.exception.OsmException; import de.blau.android.exception.OsmIllegalOperationException; import de.blau.android.filter.Filter; import de.blau.android.filter.PresetFilter; import de.blau.android.filter.TagFilter; import de.blau.android.imageryoffset.BackgroundAlignmentActionModeCallback; import de.blau.android.javascript.EvalCallback; import de.blau.android.listener.UpdateViewListener; import de.blau.android.osm.BoundingBox; import de.blau.android.osm.Node; import de.blau.android.osm.OsmElement; import de.blau.android.osm.Relation; import de.blau.android.osm.Server; import de.blau.android.osm.Server.Visibility; import de.blau.android.osm.StorageDelegator; import de.blau.android.osm.Track.TrackPoint; import de.blau.android.osm.UndoStorage; import de.blau.android.osm.Way; import de.blau.android.photos.Photo; import de.blau.android.photos.PhotoIndex; import de.blau.android.prefs.AdvancedPrefDatabase; import de.blau.android.prefs.PrefEditor; import de.blau.android.prefs.Preferences; import de.blau.android.propertyeditor.PropertyEditor; import de.blau.android.propertyeditor.PropertyEditorData; import de.blau.android.resources.DataStyle; import de.blau.android.services.TrackerService; import de.blau.android.services.TrackerService.TrackerBinder; import de.blau.android.services.TrackerService.TrackerLocationListener; import de.blau.android.tasks.Task; import de.blau.android.tasks.TaskFragment; import de.blau.android.tasks.TransferTasks; import de.blau.android.util.DateFormatter; import de.blau.android.util.FileUtil; import de.blau.android.util.FullScreenAppCompatActivity; import de.blau.android.util.GeoMath; import de.blau.android.util.MenuUtil; import de.blau.android.util.NetworkStatus; import de.blau.android.util.OAuthHelper; import de.blau.android.util.ReadFile; import de.blau.android.util.SaveFile; import de.blau.android.util.SavingHelper; import de.blau.android.util.Search.SearchResult; import de.blau.android.util.SelectFile; import de.blau.android.util.Snack; import de.blau.android.util.ThemeUtils; import de.blau.android.util.Util; import de.blau.android.views.ZoomControls; import de.blau.android.voice.Commands; import oauth.signpost.exception.OAuthException; /** * This is the main Activity from where other Activities will be started. * * @author mb */ public class Main extends FullScreenAppCompatActivity implements ServiceConnection, TrackerLocationListener, UpdateViewListener { /** * Tag used for Android-logging. */ private static final String DEBUG_TAG = Main.class.getName(); /** * Requests a {@link BoundingBox} as an activity-result. */ public static final int REQUEST_BOUNDING_BOX = 0; /** * Requests a list of {@link Tag Tags} as an activity-result. */ private static final int REQUEST_EDIT_TAG = 1; /** * Requests an activity-result. */ private static final int REQUEST_IMAGE_CAPTURE = 2; /** * Requests voice recognition. */ public static final int VOICE_RECOGNITION_REQUEST_CODE = 3; private static final double DEFAULT_BOUNDING_BOX_RADIUS = 4000000.0D; public static final String ACTION_FINISH_OAUTH = "de.blau.android.FINISH_OAUTH"; /** * Alpha value for floating action buttons workaround * We should probably find a better place for this */ public static final float FABALPHA = 0.90f; /** * Date pattern used for the image file name. */ private static final String DATE_PATTERN_IMAGE_FILE_NAME_PART = "yyyyMMdd_HHmmss"; /** * Where we install the current version of vespucci */ private static final String VERSION_FILE = "version.dat"; /** * Id for requesting permissions */ private static final int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 54321; private class ConnectivityChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { Log.d("ConnectivityChanged...","Received broadcast"); if (easyEditManager.isProcessingAction()) { easyEditManager.invalidate(); } else { supportInvalidateOptionsMenu(); } } } } private ConnectivityChangedReceiver connectivityChangedReceiver; /** Objects to handle showing device orientation. */ private SensorManager sensorManager; private Sensor magnetometer; private Sensor accelerometer; private Sensor rotation; /** * @see http://www.codingforandroid.com/2011/01/using-orientation-sensors-simple.html and http://www.journal.deviantdev.com/android-compass-azimuth-calculating/ */ private final SensorEventListener sensorListener = new SensorEventListener() { float lastAzimut = -9999; float[] acceleration; float[] geomagnetic; float[] truncatedRotationVector; @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } @Override public void onSensorChanged(SensorEvent event) { float orientation[] = new float[3]; float R[] = new float[9]; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: acceleration = event.values; break; case Sensor.TYPE_MAGNETIC_FIELD: geomagnetic = event.values; break; default: return; } if (acceleration != null && geomagnetic != null) { float I[] = new float[9]; if (!SensorManager.getRotationMatrix(R, I, acceleration, geomagnetic)) { return; } } } else if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { if (event.values.length > 4) { // See https://groups.google.com/forum/#!topic/android-developers/U3N9eL5BcJk for more information on this // // On some Samsung devices SensorManager.getRotationMatrixFromVector // appears to throw an exception if rotation vector has length > 4. // For the purposes of this class the first 4 values of the // rotation vector are sufficient (see crbug.com/335298 for details). if (truncatedRotationVector == null) { truncatedRotationVector = new float[4]; } System.arraycopy(event.values, 0, truncatedRotationVector, 0, 4); SensorManager.getRotationMatrixFromVector(R, truncatedRotationVector); } else { // calculate the rotation matrix SensorManager.getRotationMatrixFromVector(R, event.values ); } } SensorManager.getOrientation(R, orientation); float azimut = (int) ( Math.toDegrees( SensorManager.getOrientation( R, orientation )[0] ) + 360 ) % 360; map.setOrientation(azimut); // Repaint map only if orientation changed by at least 1 degree since last repaint if (Math.abs(azimut - lastAzimut) > 1) { lastAzimut = azimut; map.invalidate(); } } }; /** * webview for logging in and authorizing OAuth */ private WebView oAuthWebView; /** * our map layout */ private RelativeLayout mapLayout; /** The map View. */ private Map map; /** Detector for taps, drags, and scaling. */ private VersionedGestureDetector mDetector; /** Onscreen map zoom controls. */ private de.blau.android.views.ZoomControls zoomControls; /** * Our user-preferences. */ private Preferences prefs; /** * The manager for the EasyEdit mode */ public EasyEditManager easyEditManager; /** * Flag indicating whether the map will be re-downloaded once the activity resumes */ private static boolean redownloadOnResume; /** * Flag indicating whether data should be loaded from a file when the activity resumes. * Lock is needed because we potentially are processing results of intents before onResume runs * Set by {@link #onCreate(Bundle)}. * Overridden by {@link #redownloadOnResume}. */ private boolean loadOnResume; private final Object loadOnResumeLock = new Object(); /** * Flag indicating if we should set the view box bounding box in onResume * Again we may be already setting the view box by an intent and don't want to overwrite it */ private boolean setViewBox = true; private final Object setViewBoxLock = new Object(); private boolean showGPS; private boolean followGPS; /** * a local copy of the desired value for {@link TrackerService#setListenerNeedsGPS(boolean)}. * Will be automatically given to the tracker service on connect. */ private boolean wantLocationUpdates = false; private GeoUrlData geoData = null; private RemoteControlUrlData rcData = null; /** * Optional bottom toolbar */ private android.support.v7.widget.ActionMenuView bottomBar = null; /** * GPS FAB */ private FloatingActionButton follow; /** * The current instance of the tracker service */ private TrackerService tracker = null; public UndoListener undoListener; private BackgroundAlignmentActionModeCallback backgroundAlignmentActionModeCallback = null; // hack to protect against weird state private Location lastLocation = null; private Location locationForIntent = null; /** * Status of permissions */ private boolean locationPermissionGranted = false; private boolean askedForLocationPermission = false; private final Object locationPermissionLock = new Object(); private boolean storagePermissionGranted = false; private boolean askedForStoragePermission = false; private final Object storagePermissionLock = new Object(); /** * file we asked the camera app to create (ugly) */ private File imageFile = null; private PostAsyncActionHandler restart; // if set this is called to restart post authentication private boolean gpsChecked = false; // flag to ensure that we only check once per activity life cycle private boolean saveSync = false; // save synchronously instead of async /** * While the activity is fully active (between onResume and onPause), this stores the currently active instance */ private static Main runningInstance; /** * {@inheritDoc} */ @SuppressLint("NewApi") @Override protected void onCreate(final Bundle savedInstanceState) { Log.i(DEBUG_TAG, "onCreate " + (savedInstanceState != null?" no saved state " : " saved state exists")); // minimal support for geo: uris and JOSM style remote control geoData = (GeoUrlData)getIntent().getSerializableExtra(GeoUrlActivity.GEODATA); rcData = (RemoteControlUrlData)getIntent().getSerializableExtra(RemoteControlUrlActivity.RCDATA); prefs = new Preferences(this); if (prefs.lightThemeEnabled()) { setTheme(R.style.Theme_customMain_Light); } super.onCreate(savedInstanceState); sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE); if (sensorManager != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); if (magnetometer == null || accelerometer == null) { sensorManager = null; } } else { rotation = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); if (rotation == null) { sensorManager = null; } } } int layout = R.layout.main; if (useFullScreen(prefs)) { Log.d(DEBUG_TAG, "using full screen layout"); layout = R.layout.main_fullscreen; } LinearLayout ml = (LinearLayout) getLayoutInflater().inflate(layout, null); mapLayout = (RelativeLayout) ml.findViewById(R.id.mainMap); if (map != null) { Log.d(DEBUG_TAG, "map exists .. destroying"); map.onDestroy(); } map = new Map(this); map.setId(R.id.map_view); //Register some Listener MapTouchListener mapTouchListener = new MapTouchListener(); map.setOnTouchListener(mapTouchListener); map.setOnCreateContextMenuListener(mapTouchListener); map.setOnKeyListener(new MapKeyListener()); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { // 12 upwards map.setOnGenericMotionListener(new MotionEventListener()); } mapLayout.addView(map,0); // index 0 so that anything in the layout comes after it/on top mDetector = VersionedGestureDetector.newInstance(this, mapTouchListener); // Set up the zoom in/out controls zoomControls = new de.blau.android.views.ZoomControls(this); zoomControls.setOnZoomInClickListener(new View.OnClickListener() { @Override public void onClick(View v) { App.getLogic().zoom(Logic.ZOOM_IN); updateZoomControls(); } }); zoomControls.setOnZoomOutClickListener(new View.OnClickListener() { @Override public void onClick(View v) { App.getLogic().zoom(Logic.ZOOM_OUT); updateZoomControls(); } }); // follow GPS button setup follow = (FloatingActionButton)mapLayout.findViewById(R.id.follow); follow.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setFollowGPS(true); } }); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // currently can't be set in layout ColorStateList followTint = ContextCompat.getColorStateList(this,R.color.follow); Util.setBackgroundTintList(follow, followTint); } Util.setAlpha(follow,Main.FABALPHA); RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT); rlp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); rlp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); mapLayout.addView(zoomControls, rlp); DataStyle.getStylesFromFiles(this); // needs to happen before setContentView setContentView(ml); // Find the toolbar view inside the activity layout Toolbar toolbar = (Toolbar) findViewById(R.id.mainToolbar); // Sets the Toolbar to act as the ActionBar for this Activity window. setSupportActionBar(toolbar); if (prefs.splitActionBarEnabled()) { setBottomBar((android.support.v7.widget.ActionMenuView) findViewById(R.id.bottomToolbar)); } else { findViewById(R.id.bottomBar).setVisibility(View.GONE); } // check if first time user and display something if yes SavingHelper<String> savingHelperVersion = new SavingHelper<String>(); String lastVersion = savingHelperVersion.load(this,VERSION_FILE, false); boolean newInstall = (lastVersion == null || lastVersion.equals("")); String currentVersion = getString(R.string.app_version); boolean newVersion = (lastVersion != null) && (lastVersion.length()<5 || !lastVersion.subSequence(0,5).equals(currentVersion.subSequence(0,5))); loadOnResume = false; if (App.getLogic()==null) { Log.i(DEBUG_TAG, "onCreate - creating new logic"); App.newLogic(); } Log.i(DEBUG_TAG, "onCreate - setting new map"); App.getLogic().setPrefs(prefs); App.getLogic().setMap(map); Log.d(DEBUG_TAG,"StorageDelegator dirty is " + App.getDelegator().isDirty()); if (isLastActivityAvailable() && !App.getDelegator().isDirty()) { // data was modified while we were stopped if isDirty is true // Start loading after resume to ensure loading dialog can be removed afterwards loadOnResume = true; } else { // the following code should likely be moved to onStart or onResume if (geoData == null && rcData == null && App.getDelegator().isEmpty()) { // check if we have a position Location loc = getLastLocation(); BoundingBox box = null; if (loc != null) { try { box = GeoMath.createBoundingBoxForCoordinates(loc.getLatitude(), loc.getLongitude(), DEFAULT_BOUNDING_BOX_RADIUS, true); // km hardwired for now } catch (OsmException e) { ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } } else { // create a largish bb centered on 51.48,0 try { box = GeoMath.createBoundingBoxForCoordinates(51.48,0, DEFAULT_BOUNDING_BOX_RADIUS, false); // km hardwired for now } catch (OsmException e) { ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(e); } } openEmptyMap(box); // only show box picker if we are not showing welcome dialog if (!(newInstall || newVersion)) { gotoBoxPicker(); } } } easyEditManager = new EasyEditManager(this); // show welcome dialog if (newInstall) { // newbie, display welcome dialog Log.d(DEBUG_TAG,"showing welcome dialog"); Newbie.showDialog(this); } else if (newVersion) { Log.d(DEBUG_TAG,"new version"); NewVersion.showDialog(this); } savingHelperVersion.save(this,VERSION_FILE, getString(R.string.app_version), false); } /** * Get the best last position */ private Location getLastLocation() { LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); List<String> providers = locationManager.getProviders(true); Location bestLocation = null; for (String provider : providers) { try { 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; } /** * Loads the preferences into {@link #map}, triggers new {@inheritDoc} */ @Override protected void onStart() { Log.d(DEBUG_TAG, "onStart"); super.onStart(); prefs = new Preferences(this); App.getLogic().setPrefs(prefs); // if we have been stopped delegator and viewbox will not be set if our original Logic instance is still around map.setDelegator(App.getDelegator()); map.setViewBox(App.getLogic().getViewBox()); map.setPrefs(this, prefs); map.createOverlays(this); map.getOpenStreetMapTilesOverlay().setContrast(prefs.getContrastValue()); map.requestFocus(); undoListener = new UndoListener(); showActionBar(); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.d(DEBUG_TAG, "onNewIntent storage dirty " + App.getDelegator().isDirty()); if (ACTION_FINISH_OAUTH.equals(intent.getAction())) { Log.d(DEBUG_TAG, "onNewIntent calling finishOAuth"); finishOAuth(); return; } setIntent(intent); geoData = (GeoUrlData)getIntent().getSerializableExtra(GeoUrlActivity.GEODATA); rcData = (RemoteControlUrlData)getIntent().getSerializableExtra(RemoteControlUrlActivity.RCDATA); } @Override protected void onResume() { super.onResume(); Log.d(DEBUG_TAG, "onResume"); final Logic logic = App.getLogic(); checkPermissions(); // register received for changes in connectivity IntentFilter filter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"); connectivityChangedReceiver = new ConnectivityChangedReceiver(); registerReceiver(connectivityChangedReceiver, filter); PostAsyncActionHandler postLoadData = new PostAsyncActionHandler() { private static final long serialVersionUID = 1L; @Override public void onSuccess() { if (rcData != null || geoData != null) { processIntents(); } setupLockButton(); if (logic.getFilter()!=null) { logic.getFilter().addControls(mapLayout, new Filter.Update() { @Override public void execute() { map.invalidate(); scheduleAutoLock(); } } ); logic.getFilter().showControls(); } updateActionbarEditMode(); Mode mode = logic.getMode(); if (mode.elementsGeomEditiable() && (logic.getSelectedNode() != null || logic.getSelectedWay() != null || (logic.getSelectedRelations() != null && logic.getSelectedRelations().size() > 0))) { // need to restart whatever we were doing Log.d(DEBUG_TAG,"restarting action mode"); easyEditManager.editElements(); } else if (mode.elementsEditable()) { // de-select everything logic.deselectAll(); } } @Override public void onError() { } }; synchronized (loadOnResumeLock) { if (redownloadOnResume) { redownloadOnResume = false; logic.downloadLast(this); } else if (loadOnResume) { loadOnResume = false; PostAsyncActionHandler postLoadTasks = new PostAsyncActionHandler() { private static final long serialVersionUID = 1L; @Override public void onSuccess() { Mode mode = logic.getMode(); Task t = logic.getSelectedBug(); if (mode.elementsGeomEditiable() && t!= null) { performBugEdit(t); } } @Override public void onError() { } }; logic.loadStateFromFile(this,postLoadData); logic.loadBugsFromFile(this,postLoadTasks); } else { // loadFromFile already does this synchronized (setViewBoxLock) { App.getLogic().loadEditingState(this, setViewBox); } postLoadData.onSuccess(); map.invalidate(); } } synchronized (setViewBoxLock) { // reset in any case setViewBox = true; } logic.updateProfile(); map.updateProfile(); runningInstance = this; if (getTracker() != null) { getTracker().setListener(this); lastLocation = getTracker().getLastLocation(); } setShowGPS(prefs.getShowGPS()); map.setKeepScreenOn(prefs.isKeepScreenOnEnabled()); scheduleAutoLock(); if (prefs.getEnableTagFilter() && logic.getMode() != Mode.MODE_INDOOR) { Filter.Update updater = new Filter.Update() { @Override public void execute() { map.invalidate(); scheduleAutoLock(); } }; logic.setFilter(new TagFilter(this)); logic.getFilter().addControls(getMapLayout(), updater); } } /** * Check if we have fine location permission and ask for it if not * Side effect binds to TrackerService */ private void checkPermissions() { final List<String> permissionsList = new ArrayList<String>(); synchronized (locationPermissionLock) { locationPermissionGranted = false; if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // Should we show an explanation? if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { // for now we just repeat the request (max once) if (!askedForLocationPermission) { permissionsList.add(Manifest.permission.ACCESS_FINE_LOCATION); askedForLocationPermission = true; } } else { permissionsList.add(Manifest.permission.ACCESS_FINE_LOCATION); } } else { // permission was already given bindService(new Intent(this, TrackerService.class), this, BIND_AUTO_CREATE); locationPermissionGranted = true; } } synchronized (storagePermissionLock) { storagePermissionGranted = false; if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Should we show an explanation? if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { // for now we just repeat the request (max once) if (!askedForStoragePermission) { permissionsList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); askedForStoragePermission = true; } } else { permissionsList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); } } else { // permission was already given storagePermissionGranted = true; } } if (permissionsList.size() > 0) { ActivityCompat.requestPermissions(this, permissionsList.toArray(new String[permissionsList.size()]), REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); } } /** * Check if we have write to external permission and ask for it if not */ void checkStoragePermission() { } /** * Process geo an JOSM remote control intents */ private void processIntents() { final Logic logic = App.getLogic(); if (geoData != null) { Log.d(DEBUG_TAG,"got position from geo: url " + geoData.getLat() + "/" + geoData.getLon() + " storage dirty is " + App.getDelegator().isDirty()); if (prefs.getDownloadRadius() != 0) { // download BoundingBox bbox; try { bbox = GeoMath.createBoundingBoxForCoordinates(geoData.getLat(), geoData.getLon(), prefs.getDownloadRadius(), true); ArrayList<BoundingBox> bbList = new ArrayList<BoundingBox>(App.getDelegator().getBoundingBoxes()); ArrayList<BoundingBox> bboxes = null; if (App.getDelegator().isEmpty()) { bboxes = new ArrayList<BoundingBox>(); bboxes.add(bbox); } else { bboxes = BoundingBox.newBoxes(bbList, bbox); } if (bboxes != null && bboxes.size() > 0) { logic.downloadBox(this, bbox, true, null); if (prefs.areBugsEnabled()) { // always adds bugs for now TransferTasks.downloadBox(this, prefs.getServer(), bbox, true, new PostAsyncActionHandler() { private static final long serialVersionUID = 1L; @Override public void onSuccess() { getMap().invalidate(); } @Override public void onError() { } }); } } else { Log.d(DEBUG_TAG,"no bbox to download"); logic.getViewBox().setBorders(getMap(), bbox); map.invalidate(); } } catch (OsmException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { Log.d(DEBUG_TAG,"moving to position"); map.getViewBox().moveTo(getMap(), (int)(geoData.getLon()*1E7), (int)(geoData.getLat()*1E7)); map.invalidate(); } geoData=null; // zap to stop repeated downloads } if (rcData != null) { Log.d(DEBUG_TAG,"got data from remote control url " + rcData.getBox() + " load " + rcData.load()); StorageDelegator delegator = App.getDelegator(); ArrayList<BoundingBox> bbList = new ArrayList<BoundingBox>(delegator.getBoundingBoxes()); BoundingBox loadBox = rcData.getBox(); if (loadBox != null) { if (rcData.load()) { // download ArrayList<BoundingBox> bboxes = BoundingBox.newBoxes(bbList, loadBox); if (bboxes != null && (bboxes.size() > 0 || delegator.isEmpty())) { // only download if we haven't yet logic.downloadBox(this, rcData.getBox(), true /* logic.delegator.isDirty() */, new PostAsyncActionHandler(){ private static final long serialVersionUID = 1L; @Override public void onSuccess(){ rcDataEdit(rcData); rcData=null; // zap to stop repeated downloads } @Override public void onError() { } }); } else { rcDataEdit(rcData); rcData=null; // zap to stop repeated downloads } } else { // zoom map.getViewBox().setBorders(getMap(),rcData.getBox()); map.invalidate(); rcData=null; // zap to stop repeated downloads } } else { Log.d(DEBUG_TAG,"RC box is null"); rcDataEdit(rcData); rcData=null; // zap to stop repeated downloads } } } /** * Parse the parameters of a JOSM remote control URL and select and edit the OSM objects. * @param rcData Data of a remote control data URL. */ private void rcDataEdit(RemoteControlUrlData rcData) { BoundingBox box = rcData.getBox(); if (box != null) { map.getViewBox().setBorders(getMap(),box); } final Logic logic = App.getLogic(); if (rcData.getSelect() != null) { // need to actually switch to easyeditmode if (!logic.getMode().elementsGeomEditiable()) { // TODO there might be states in which we don't want to exit which ever mode we are in setMode(this, Mode.MODE_EASYEDIT); } logic.setSelectedNode(null); logic.setSelectedWay(null); logic.setSelectedRelation(null); StorageDelegator storageDelegator = App.getDelegator(); for (String s:rcData.getSelect().split(",")) { // see http://wiki.openstreetmap.org/wiki/JOSM/Plugins/RemoteControl if (s!=null) { Log.d(DEBUG_TAG,"rc select: " + s); try { if (s.startsWith("node")) { long id = Long.valueOf(s.substring(Node.NAME.length())); Node n = (Node) storageDelegator.getOsmElement(Node.NAME, id); if (n != null) { logic.addSelectedNode(n); } } else if (s.startsWith("way")) { long id = Long.valueOf(s.substring(Way.NAME.length())); Way w = (Way) storageDelegator.getOsmElement(Way.NAME, id); if (w != null) { logic.addSelectedWay(w); } } else if (s.startsWith("relation")) { long id = Long.valueOf(s.substring(Relation.NAME.length())); Relation r = (Relation) storageDelegator.getOsmElement(Relation.NAME, id); if (r != null) { logic.addSelectedRelation(r); } } } catch (NumberFormatException nfe) { Log.d(DEBUG_TAG,"Parsing " + s + " caused " + nfe); // not much more we can do here } } } easyEditManager.editElements(); } } @Override protected void onPause() { descheduleAutoLock(); Log.d(DEBUG_TAG, "onPause mode " + App.getLogic().getMode()); runningInstance = null; try { unregisterReceiver(connectivityChangedReceiver); } catch (Exception e) { // FIXME if onPause gets called before onResume has registered the Receiver // unregisterReceiver will throw an exception, a better fix would likely to // register earlier, but that may not help } disableLocationUpdates(); if (getTracker() != null) getTracker().setListener(null); // always save editing state App.getLogic().saveEditingState(this); // onPause is the last lifecycle callback guaranteed to be called on pre-honeycomb devices // on honeycomb and later, onStop is also guaranteed to be called, so we can defer saving. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) saveData(); super.onPause(); } @Override protected void onStop() { Log.d(DEBUG_TAG, "onStop"); // editing state has been saved in onPause // On devices with Android versions before Honeycomb, we already save data in onPause if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) saveData(); super.onStop(); } @Override protected void onDestroy() { Log.d(DEBUG_TAG, "onDestroy"); map.onDestroy(); if (getTracker() != null) getTracker().setListener(null); try { unbindService(this); } catch (Exception ignored) { Log.d(DEBUG_TAG, "Ignored " + ignored); } super.onDestroy(); } /** * Save current data (state, downloaded data, changes, ...) to file(s) */ private void saveData() { Log.i(DEBUG_TAG, "saving data sync="+saveSync); final Logic logic = App.getLogic(); if (saveSync) { logic.save(this); } else { logic.saveAsync(this); } } /** * Update the state of the onscreen zoom controls to reflect their ability * to zoom in/out. */ private void updateZoomControls() { final Logic logic = App.getLogic(); getControls().setIsZoomInEnabled(logic.canZoom(Logic.ZOOM_IN)); getControls().setIsZoomOutEnabled(logic.canZoom(Logic.ZOOM_OUT)); } // @Override // public Object onRetainNonConfigurationInstance() { // Log.d(DEBUG_TAG, "onRetainNonConfigurationInstance"); // return logic; // } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); App.getLogic().setMap(map); Log.d(DEBUG_TAG, "onConfigurationChanged"); if (easyEditManager.isProcessingAction()) { easyEditManager.invalidate(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Log.d(DEBUG_TAG, "onRequestPermissionsResult"); super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: for (int i=0;i<permissions.length;i++) { if (permissions[i].equals(Manifest.permission.ACCESS_FINE_LOCATION) && grantResults[i] == PackageManager.PERMISSION_GRANTED) { // permission was granted :) bindService(new Intent(this, TrackerService.class), this, BIND_AUTO_CREATE); synchronized (locationPermissionLock) { locationPermissionGranted = true; } } // if not granted do nothing for now if (permissions[i].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted :) synchronized (storagePermissionLock) { storagePermissionGranted = true; } } // if not granted do nothing for now } break; } triggerMenuInvalidation(); // update menus } /** * Sets up the Action Bar. */ private void showActionBar() { Log.d(DEBUG_TAG, "showActionBar"); final ActionBar actionbar = getSupportActionBar(); actionbar.setDisplayShowHomeEnabled(true); actionbar.setDisplayShowTitleEnabled(false); actionbar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM|ActionBar.DISPLAY_SHOW_HOME); setupLockButton(); if (prefs.splitActionBarEnabled()) { actionbar.hide(); } else { actionbar.show(); } FloatingActionButton follow = getFollowButton(); if (follow != null) { if (ensureGPSProviderEnabled()) { RelativeLayout.LayoutParams params = (LayoutParams) follow.getLayoutParams(); if (getString(R.string.follow_GPS_left).equals(prefs.followGPSbuttonPosition())) { params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,0); params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE); } else if (getString(R.string.follow_GPS_right).equals(prefs.followGPSbuttonPosition())) { params.addRule(RelativeLayout.ALIGN_PARENT_LEFT,0); params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE); } else if (getString(R.string.follow_GPS_none).equals(prefs.followGPSbuttonPosition())) { follow.hide(); } follow.setLayoutParams(params); } else { follow.hide(); } } } /** * Setups up the listeners for click and longclick on the lock icon including mode switching logic */ @SuppressLint("InflateParams") private void setupLockButton() { final Logic logic = App.getLogic(); Mode mode = logic.getMode(); Log.d(DEBUG_TAG, "setupLockButton mode " + mode); //ToggleButton lock = setLock(mode); final FloatingActionButton lock = setLock(mode); if (lock == null) { return; //FIXME not good but no other alternative right now, already logged in setLock } lock.setTag(mode.tag()); StateListDrawable states = new StateListDrawable(); states.addState(new int[] {android.R.attr.state_pressed}, ContextCompat.getDrawable(this, mode.iconResourceId())); states.addState(new int[] {0}, ContextCompat.getDrawable(this, R.drawable.locked_opaque)); lock.setImageDrawable(states); // lock.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View b) { Log.d(DEBUG_TAG, "Lock pressed " + b.getClass().getName()); int[] drawableState = ((FloatingActionButton)b).getDrawableState(); Log.d(DEBUG_TAG, "Lock state length " + drawableState.length + " " + (drawableState.length==1? Integer.toHexString(drawableState[0]):"")); if(drawableState.length == 0 || drawableState[0]!=android.R.attr.state_pressed){ Mode mode = Mode.modeForTag((String)b.getTag()); logic.setMode(Main.this, mode); ((FloatingActionButton)b).setImageState(new int[]{android.R.attr.state_pressed}, false); logic.setLocked(false); } else { logic.setLocked(true); ((FloatingActionButton)b).setImageState(new int[]{0}, false); } onEditModeChanged(); map.invalidate(); } }); lock.setLongClickable(true); lock.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View b) { Log.d(DEBUG_TAG, "Lock long pressed " + b.getClass().getName()); final Logic logic = App.getLogic(); Mode mode = logic.getMode(); ArrayList<Mode> allModes = new ArrayList<Mode>(Arrays.asList(Mode.values())); int position = allModes.indexOf(mode); int size = allModes.size(); // find the next usable mode for (int i=1;i<allModes.size();i++) { Mode newMode = allModes.get((position+i) % size); if (newMode.isSubModeOf()==null && newMode.isEnabled()) { mode = newMode; break; } } logic.setMode(Main.this, mode); b.setTag(mode.tag()); StateListDrawable states = new StateListDrawable(); states.addState(new int[] {android.R.attr.state_pressed}, ContextCompat.getDrawable(Main.this, mode.iconResourceId())); states.addState(new int[] {}, ContextCompat.getDrawable(Main.this, R.drawable.locked_opaque)); lock.setImageDrawable(states); if (logic.isLocked()) { ((FloatingActionButton)b).setImageState(new int[]{0}, false); } else { ((FloatingActionButton)b).setImageState(new int[]{android.R.attr.state_pressed}, false); } onEditModeChanged(); return true; } }); } public FloatingActionButton getLock() { return (FloatingActionButton) findViewById(R.id.floatingLock); } /** * Set lock button to locked or unlocked depending on the edit mode * @param mode Program mode. * @return Button to display checked/unchecked states. */ private FloatingActionButton setLock(Mode mode) { FloatingActionButton lock = getLock(); if (lock==null) { Log.d(DEBUG_TAG, "couldn't find lock button"); return null; } Logic logic = App.getLogic(); if (logic.isLocked()) { lock.setImageState(new int[]{0}, false); } else { lock.setImageState(new int[]{android.R.attr.state_pressed}, false); } logic.setMode(this, mode); return lock; // for convenience } public void setMode(Main main, Mode mode) { App.getLogic().setMode(main, mode); } private void updateActionbarEditMode() { Log.d(DEBUG_TAG, "updateActionbarEditMode"); Mode mode = App.getLogic().getMode(); setLock(mode); supportInvalidateOptionsMenu(); } public static void onEditModeChanged() { Log.d(DEBUG_TAG, "onEditModeChanged"); if (runningInstance != null) runningInstance.updateActionbarEditMode(); } BottomBarClickListener bottomBarListener; @Override public boolean onPrepareOptionsMenu(final Menu m) { if (bottomBarListener == null && getBottomBar() != null) { // NOTE doing this here tries to keep a valid reference to the activity // doing it here should guarantee that it always works bottomBarListener = new BottomBarClickListener(this); getBottomBar().setOnMenuItemClickListener(bottomBarListener); } return true; } /** * Creates the menu from the XML file "main_menu.xml".<br> {@inheritDoc} * * Note for not entirely clear reasons *:setShowAsAction doesn't work in the menu definition and has to be done programmatically here. */ @SuppressLint("InflateParams") @Override public boolean onCreateOptionsMenu(final Menu m) { Log.d(DEBUG_TAG, "onCreateOptionsMenu"); // determine how man icons have room MenuUtil menuUtil = new MenuUtil(this); Menu menu = m; if (getBottomBar() != null) { menu = getBottomBar().getMenu(); Log.d(DEBUG_TAG,"inflated main menu on to bottom toolbar"); } if (menu.size() == 0) { menu.clear(); final MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); } boolean networkConnected = NetworkStatus.isConnected(this); boolean gpsProviderEnabled = ensureGPSProviderEnabled() && locationPermissionGranted; // just as good as any other place to check this if (gpsProviderEnabled) { showFollowButton(); } else { hideFollowButton(); } menu.findItem(R.id.menu_gps_show).setEnabled(gpsProviderEnabled).setChecked(showGPS); menu.findItem(R.id.menu_gps_follow).setEnabled(gpsProviderEnabled).setChecked(followGPS); menu.findItem(R.id.menu_gps_goto).setEnabled(gpsProviderEnabled); menu.findItem(R.id.menu_gps_start).setEnabled(getTracker() != null && !getTracker().isTracking() && gpsProviderEnabled); menu.findItem(R.id.menu_gps_pause).setEnabled(getTracker() != null && getTracker().isTracking() && gpsProviderEnabled); menu.findItem(R.id.menu_gps_autodownload).setEnabled(getTracker() != null && gpsProviderEnabled && networkConnected).setChecked(autoDownload()); menu.findItem(R.id.menu_transfer_bugs_autodownload).setEnabled(getTracker() != null && gpsProviderEnabled && networkConnected).setChecked(bugAutoDownload()); menu.findItem(R.id.menu_gps_clear).setEnabled(getTracker() != null && getTracker().getTrackPoints() != null && getTracker().getTrackPoints().size() > 0); menu.findItem(R.id.menu_gps_goto_start).setEnabled(getTracker() != null && getTracker().getTrackPoints() != null && getTracker().getTrackPoints().size() > 0); menu.findItem(R.id.menu_gps_import).setEnabled(getTracker() != null); menu.findItem(R.id.menu_gps_upload).setEnabled(getTracker() != null && getTracker().getTrackPoints() != null && getTracker().getTrackPoints().size() > 0 && NetworkStatus.isConnected(this)); final Logic logic = App.getLogic(); MenuItem undo = menu.findItem(R.id.menu_undo); undo.setVisible(!logic.isLocked() && (logic.getUndo().canUndo() || logic.getUndo().canRedo())); View undoView = MenuItemCompat.getActionView(undo); if (undoView == null) { // FIXME this is a temp workaround for pre-11 Android, we could probably simply always do the following Log.d(DEBUG_TAG,"undoView null"); Context context = new ContextThemeWrapper(this, prefs.lightThemeEnabled() ? R.style.Theme_customMain_Light : R.style.Theme_customMain); undoView = ((LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.undo_action_view, null); } undoView.setOnClickListener(undoListener); undoView.setOnLongClickListener(undoListener); final Server server = prefs.getServer(); if (server.hasOpenChangeset()) { menu.findItem(R.id.menu_transfer_close_changeset).setVisible(true); } else { menu.findItem(R.id.menu_transfer_close_changeset).setVisible(false); } menu.findItem(R.id.menu_transfer_download_current).setEnabled(networkConnected); menu.findItem(R.id.menu_transfer_download_current_add).setEnabled(networkConnected); menu.findItem(R.id.menu_transfer_download_other).setEnabled(networkConnected); // note: isDirty is not a good indicator of if if there is really something to upload menu.findItem(R.id.menu_transfer_upload).setEnabled(networkConnected && !App.getDelegator().getApiStorage().isEmpty()); menu.findItem(R.id.menu_transfer_bugs_download_current).setEnabled(networkConnected); menu.findItem(R.id.menu_transfer_bugs_upload).setEnabled(networkConnected && App.getTaskStorage().hasChanges()); menu.findItem(R.id.menu_voice).setVisible(false); // don't display button for now // experimental menu.findItem(R.id.menu_voice).setEnabled(networkConnected && prefs.voiceCommandsEnabled()).setVisible(prefs.voiceCommandsEnabled()); // the following depends on us having permission to write to "external" storage menu.findItem(R.id.menu_transfer_export).setEnabled(storagePermissionGranted); menu.findItem(R.id.menu_transfer_save_file).setEnabled(storagePermissionGranted); menu.findItem(R.id.menu_transfer_save_notes_all).setEnabled(storagePermissionGranted); menu.findItem(R.id.menu_transfer_save_notes_new_and_changed).setEnabled(storagePermissionGranted); menu.findItem(R.id.menu_gps_export).setEnabled(storagePermissionGranted); Filter filter = logic.getFilter(); if (filter != null && filter instanceof TagFilter && !prefs.getEnableTagFilter()) { // something is wrong, try to sync prefs.enableTagFilter(true); Log.d(DEBUG_TAG,"had to resync tagfilter pref"); } menu.findItem(R.id.menu_enable_tagfilter).setEnabled(logic.getMode() != Mode.MODE_INDOOR).setChecked(prefs.getEnableTagFilter() && logic.getFilter() instanceof TagFilter); menu.findItem(R.id.menu_enable_presetfilter).setEnabled(logic.getMode() != Mode.MODE_INDOOR).setChecked(prefs.getEnablePresetFilter() && logic.getFilter() instanceof PresetFilter); // enable the JS console menu entry menu.findItem(R.id.tag_menu_js_console).setEnabled(prefs.isJsConsoleEnabled()); menuUtil.setShowAlways(menu); // only show camera icon if we have a camera, and a camera app is installed PackageManager pm = getPackageManager(); Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA) && cameraIntent.resolveActivity(getPackageManager()) != null) { MenuItemCompat.setShowAsAction(menu.findItem(R.id.menu_camera),prefs.showCameraAction() ? MenuItemCompat.SHOW_AS_ACTION_ALWAYS: MenuItemCompat.SHOW_AS_ACTION_NEVER); } else { MenuItem mi = menu.findItem(R.id.menu_camera).setVisible(false); MenuItemCompat.setShowAsAction(mi,MenuItemCompat.SHOW_AS_ACTION_NEVER); } if (getBottomBar()!=null) { //menuUtil.evenlyDistributedToolbar(getBottomToolbar()); } return true; } /** * {@inheritDoc} */ @Override public boolean onOptionsItemSelected(final MenuItem item) { Log.d(DEBUG_TAG, "onOptionsItemSelected"); final Server server = prefs.getServer(); final Logic logic = App.getLogic(); StorageDelegator delegator = App.getDelegator(); if (delegator == null) { // very unlikely error condition Log.e(DEBUG_TAG,"onOptionsItemSelected delegator null"); return true; } switch (item.getItemId()) { case R.id.menu_config: PrefEditor.start(this,getMap().getViewBox()); return true; case R.id.menu_find: SearchForm.showDialog(this, map.getViewBox(), new de.blau.android.util.SearchItemFoundCallback() { private static final long serialVersionUID = 1L; @Override public void onItemFound(SearchResult sr) { // turn this off or else we get bounced back to our current GPS position setFollowGPS(false); getMap().setFollowGPS(false); logic.setZoom(getMap(), 19); getMap().getViewBox().moveTo(getMap(), (int) (sr.getLon() * 1E7d), (int)(sr.getLat()* 1E7d)); getMap().invalidate(); } }); return true; case R.id.menu_enable_tagfilter: case R.id.menu_enable_presetfilter: Filter newFilter = null; switch (item.getItemId()) { case R.id.menu_enable_tagfilter: Log.d(DEBUG_TAG,"filter menu tag"); if (prefs.getEnableTagFilter()) { // already selected turn off prefs.enableTagFilter(false); item.setChecked(false); } else { prefs.enableTagFilter(true); item.setChecked(true); prefs.enablePresetFilter(false); newFilter = new TagFilter(this); } break; case R.id.menu_enable_presetfilter: Log.d(DEBUG_TAG,"filter menu preset"); if (prefs.getEnablePresetFilter()) { // already selected turn off prefs.enablePresetFilter(false); item.setChecked(false); } else { prefs.enablePresetFilter(true); item.setChecked(true); prefs.enableTagFilter(false); newFilter = new PresetFilter(this); } break; } Filter currentFilter = logic.getFilter(); if (currentFilter != null) { currentFilter.saveState(); currentFilter.hideControls(); currentFilter.removeControls(); logic.setFilter(null); } if (newFilter != null) { Filter.Update updater = new Filter.Update() { @Override public void execute() { map.invalidate(); scheduleAutoLock(); } }; logic.setFilter(newFilter); logic.getFilter().addControls(getMapLayout(), updater); logic.getFilter().showControls(); } triggerMenuInvalidation(); map.invalidate(); return true; case R.id.menu_share: BoundingBox box = map.getViewBox(); double[] lonLat = new double[2]; lonLat[0] = ((box.getRight() - box.getLeft())/2 + box.getLeft())/1E7; lonLat[1] = ((box.getTop() - box.getBottom())/2 + box.getBottom())/1E7; // rough Util.sharePosition(this, lonLat); break; case R.id.menu_help: HelpViewer.start(this, R.string.help_main); return true; // case R.id.menu_voice: // // return true; case R.id.menu_camera: Intent startCamera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); try { imageFile = getImageFile(); Uri photoUri = FileProvider.getUriForFile(this, "de.blau.android.osmeditor4android.provider", imageFile); if (photoUri != null) { startCamera.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); startCamera.putExtra(MediaStore.EXTRA_OUTPUT,photoUri); startActivityForResult(startCamera, REQUEST_IMAGE_CAPTURE); } } catch (Exception ex) { try { Snack.barError(this, getResources().getString(R.string.toast_camera_error, ex.getMessage())); } catch (Exception e) { // protect against translation errors } } return true; case R.id.menu_gps_show: toggleShowGPS(); return true; case R.id.menu_gps_follow: toggleFollowGPS(); return true; case R.id.menu_gps_goto: Location gotoLoc = null; if (getTracker() != null) { gotoLoc = getTracker().getLastLocation(); } else if (ensureGPSProviderEnabled()) { gotoLoc = getLastLocation(); } // else moan? without GPS enabled this shouldn't be selectable currently if (gotoLoc != null) { map.getViewBox().moveTo(getMap(), (int) (gotoLoc.getLongitude() * 1E7d), (int) (gotoLoc.getLatitude() * 1E7d)); logic.setZoom(getMap(), 19); map.setLocation(gotoLoc); map.invalidate(); } return true; case R.id.menu_gps_start: if (getTracker() != null && ensureGPSProviderEnabled()) { getTracker().startTracking(); setFollowGPS(true); } return true; case R.id.menu_gps_pause: if (getTracker() != null && ensureGPSProviderEnabled()) { getTracker().stopTracking(false); triggerMenuInvalidation(); } return true; case R.id.menu_gps_clear: if (getTracker() != null) getTracker().stopTracking(true); triggerMenuInvalidation(); map.invalidate(); return true; case R.id.menu_gps_upload: if (server != null && server.isLoginSet()) { if (server.needOAuthHandshake()) { oAuthHandshake(server, new PostAsyncActionHandler() { private static final long serialVersionUID = 1L; @Override public void onSuccess() { GpxUpload.showDialog(Main.this); } @Override public void onError() { } }); if (server.getOAuth()) { // if still set Snack.barError(this, R.string.toast_oauth); return true; } } GpxUpload.showDialog(this); // performTrackUpload("Test","Test",Visibility.PUBLIC); } else { ErrorAlert.showDialog(this,ErrorCodes.NO_LOGIN_DATA); } return true; case R.id.menu_gps_export: if (getTracker() != null) { SavingHelper.asyncExport(this, getTracker()); } return true; case R.id.menu_gps_import: descheduleAutoLock(); SelectFile.read(this, R.string.config_gpxPreferredDir_key, new ReadFile(){ private static final long serialVersionUID = 1L; @Override public boolean read(Uri fileUri) { // Get the Uri of the selected file Log.d(DEBUG_TAG, "Read gpx file Uri: " + fileUri.toString()); if (getTracker() != null) { if (getTracker().getTrackPoints().size() > 0 ) { ImportTrack.showDialog(Main.this, fileUri); } else { getTracker().stopTracking(false); try { getTracker().importGPXFile(Main.this, fileUri); } catch (FileNotFoundException e) { try { Snack.barError(Main.this, getResources().getString(R.string.toast_file_not_found, fileUri.toString())); } catch (Exception ex) { // protect against translation errors } } } SelectFile.savePref(prefs, R.string.config_gpxPreferredDir_key, fileUri); } map.invalidate(); return true; }}); return true; case R.id.menu_gps_goto_start: List<TrackPoint> l = tracker.getTrackPoints(); if (l != null && l.size() > 0) { Log.d(DEBUG_TAG,"Going to start of track"); setFollowGPS(false); map.setFollowGPS(false); map.getViewBox().moveTo(getMap(), l.get(0).getLon(), l.get(0).getLat()); logic.setZoom(getMap(), 19); map.invalidate(); } return true; case R.id.menu_gps_autodownload: prefs.setAutoDownload(!autoDownload()); startStopAutoDownload(); return true; case R.id.menu_transfer_download_current: onMenuDownloadCurrent(false); return true; case R.id.menu_transfer_download_current_add: onMenuDownloadCurrent(true); return true; case R.id.menu_transfer_download_other: gotoBoxPicker(); return true; case R.id.menu_transfer_upload: confirmUpload(); return true; case R.id.menu_transfer_close_changeset: if (server.hasOpenChangeset()) { // fail silently if it doesn't work, next upload will open a new changeset in any case new AsyncTask<Void, Integer, Void>() { @Override protected Void doInBackground(Void... params) { try { server.closeChangeset(); } catch (MalformedURLException e) { } catch (ProtocolException e) { } catch (IOException e) { } return null; } @Override protected void onPostExecute(Void result) { triggerMenuInvalidation(); } }.execute(); } return true; case R.id.menu_transfer_export: { SavingHelper.asyncExport(this, delegator); return true; } case R.id.menu_transfer_read_file: descheduleAutoLock(); // showFileChooser(READ_OSM_FILE_SELECT_CODE); SelectFile.read(this, R.string.config_osmPreferredDir_key, new ReadFile(){ private static final long serialVersionUID = 1L; @Override public boolean read(Uri fileUri) { try { logic.readOsmFile(Main.this, fileUri, false); } catch (FileNotFoundException e) { try { Snack.barError(Main.this, getResources().getString(R.string.toast_file_not_found, fileUri.toString())); } catch (Exception ex) { // protect against translation errors } } SelectFile.savePref(prefs, R.string.config_osmPreferredDir_key, fileUri); map.invalidate(); return true; }}); return true; case R.id.menu_transfer_save_file: descheduleAutoLock(); SelectFile.save(this, R.string.config_osmPreferredDir_key, new SaveFile(){ private static final long serialVersionUID = 1L; @Override public boolean save(Uri fileUri) { App.getLogic().writeOsmFile(Main.this, fileUri.getPath(), null); SelectFile.savePref(prefs, R.string.config_osmPreferredDir_key, fileUri); return true; }}); return true; case R.id.menu_transfer_bugs_download_current: TransferTasks.downloadBox(this, prefs.getServer(), map.getViewBox().copy(), true, new PostAsyncActionHandler() { private static final long serialVersionUID = 1L; @Override public void onSuccess() { map.invalidate(); } @Override public void onError() { } }); return true; case R.id.menu_transfer_bugs_upload: if (App.getTaskStorage().hasChanges()) { TransferTasks.upload(this, server, null); } else { Snack.barInfo(this, R.string.toast_no_changes); } return true; case R.id.menu_transfer_bugs_clear: if (App.getTaskStorage().hasChanges()) { // FIXME show a dialog and allow override Snack.barError(this, R.string.toast_unsaved_changes); return true; } App.getTaskStorage().reset(); map.invalidate(); return true; case R.id.menu_transfer_bugs_autodownload: prefs.setBugAutoDownload(!bugAutoDownload()); startStopBugAutoDownload(); return true; case R.id.menu_transfer_save_notes_all: case R.id.menu_transfer_save_notes_new_and_changed: if (App.getTaskStorage() == null) return true; descheduleAutoLock(); SelectFile.save(this, R.string.config_notesPreferredDir_key, new SaveFile(){ private static final long serialVersionUID = 1L; @Override public boolean save(Uri fileUri) { TransferTasks.writeOsnFile(Main.this, item.getItemId()==R.id.menu_transfer_save_notes_all,fileUri.getPath()); SelectFile.savePref(prefs, R.string.config_notesPreferredDir_key, fileUri); return true; }}); return true; case R.id.menu_undo: // should not happen undoListener.onClick(null); return true; case R.id.menu_tools_flush_background_tile_cache: map.getOpenStreetMapTilesOverlay().flushTileCache(this); return true; case R.id.menu_tools_flush_overlay_tile_cache: map.getOpenStreetMapOverlayTilesOverlay().flushTileCache(this); return true; case R.id.menu_tools_background_align: Mode oldMode = logic.getMode() != Mode.MODE_ALIGN_BACKGROUND ? logic.getMode() : Mode.MODE_EASYEDIT; // protect against weird state backgroundAlignmentActionModeCallback = new BackgroundAlignmentActionModeCallback(this, oldMode); logic.setMode(this, Mode.MODE_ALIGN_BACKGROUND); //NOTE needs to be after instance creation startSupportActionMode(getBackgroundAlignmentActionModeCallback()); return true; case R.id.menu_tools_background_properties: BackgroundProperties.showDialog(this); return true; case R.id.menu_tools_oauth_reset: // reset the current OAuth tokens if (server.getOAuth()) { AdvancedPrefDatabase prefdb = new AdvancedPrefDatabase(this); prefdb.setAPIAccessToken(null, null); } else { Snack.barError(this, R.string.toast_oauth_not_enabled); } return true; case R.id.menu_tools_oauth_authorisation: // immediately start authorization handshake if (server.getOAuth()) { oAuthHandshake(server, null); } else { Snack.barError(this, R.string.toast_oauth_not_enabled); } return true; case R.id.tag_menu_js_console: Main.showJsConsole(this); return true; } return false; } public static void showJsConsole(final Main main) { main.descheduleAutoLock(); de.blau.android.javascript.Utils.jsConsoleDialog(main, R.string.js_console_msg_live, new EvalCallback() { @Override public String eval(String input) { String result = de.blau.android.javascript.Utils.evalString(main, "JS Console", input, App.getLogic()); main.runOnUiThread(new Runnable() { @Override public void run() { main.getMap().invalidate(); main.scheduleAutoLock(); } }); return result; } }); } private void startVoiceRecognition() { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); try { startActivityForResult(intent, VOICE_RECOGNITION_REQUEST_CODE); } catch (Exception ex) { Log.d(DEBUG_TAG,"Caught exception " + ex); Snack.barError(this, R.string.toast_no_voice); } } private File getImageFile() throws IOException { File outDir = FileUtil.getPublicDirectory(); outDir = FileUtil.getPublicDirectory(outDir, Paths.DIRECTORY_PATH_PICTURES); String imageFileName = DateFormatter.getFormattedString(DATE_PATTERN_IMAGE_FILE_NAME_PART); File imageFile = File.createTempFile(imageFileName, Paths.FILE_EXTENSION_IMAGE, outDir); Log.d(DEBUG_TAG, "getImageFile " + imageFile.getAbsolutePath()); return imageFile; } private void showFileChooser(int purpose) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); intent.addCategory(Intent.CATEGORY_OPENABLE); try { // startActivityForResult( // Intent.createChooser(intent, purpose == WRITE_OSM_FILE_SELECT_CODE ? getString(R.string.save_file) : getString(R.string.read_file)), // purpose); startActivityForResult(intent,purpose); } catch (android.content.ActivityNotFoundException ex) { // Potentially direct the user to the Market with a Dialog Snack.barError(this, R.string.toast_missing_filemanager); } } private void startStopAutoDownload() { Log.d(DEBUG_TAG, "autoDownload"); if (getTracker() != null && ensureGPSProviderEnabled()) { if (autoDownload()) { getTracker().startAutoDownload(); } else { getTracker().stopAutoDownload(); } } } private boolean autoDownload() { return prefs.getAutoDownload(); } private void startStopBugAutoDownload() { Log.d(DEBUG_TAG, "bugAutoDownload"); if (getTracker() != null && ensureGPSProviderEnabled()) { if (bugAutoDownload()) { getTracker().startBugAutoDownload(); } else { getTracker().stopBugAutoDownload(); } } } private boolean bugAutoDownload() { return prefs.getBugAutoDownload(); } private void setShowGPS(boolean show) { if (show && !ensureGPSProviderEnabled()) { show = false; } showGPS = show; Log.d(DEBUG_TAG, "showGPS: "+ show); if (show) { enableLocationUpdates(); } else { setFollowGPS(false); map.setLocation(null); disableLocationUpdates(); } prefs.setShowGPS(show); map.invalidate(); triggerMenuInvalidation(); } private boolean getShowGPS() { return showGPS; } /** * Checks if GPS is enabled in the settings. * If not, returns false and shows location settings. * @return true if GPS is enabled, false if not */ private boolean ensureGPSProviderEnabled() { try { LocationManager locationManager = (LocationManager)getSystemService(LOCATION_SERVICE); if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { gpsChecked = false; return true; } else if (locationManager.getProvider(LocationManager.GPS_PROVIDER) != null){ // check if there is a GPS providers at all if (!gpsChecked && !prefs.leaveGpsDisabled()) { gpsChecked = true; startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); } return false; } // // Log.d(DEBUG_TAG,"No GPS provider"); // List<String> providers = locationManager.getAllProviders(); // for (String p:providers) { // Log.d(DEBUG_TAG,"Provider: " + p); // } return false; } catch (Exception e) { Log.d(DEBUG_TAG, "Error when checking for GPS, assuming GPS not available", e); Snack.barInfo(this, R.string.gps_failure); return false; } } public void setFollowGPS(boolean follow) { // Log.d(DEBUG_TAG,"setFollowGPS from " + followGPS + " to " + follow); if (followGPS != follow) { followGPS = follow; if (follow) { setShowGPS(true); } FloatingActionButton followButton = getFollowButton(); if (followButton != null) { followButton.setEnabled(!follow); } map.setFollowGPS(follow); triggerMenuInvalidation(); } if (follow && lastLocation != null) { // update if we are returning from pause/stop Log.d(DEBUG_TAG,"Setting lastLocation"); onLocationChanged(lastLocation); } } public boolean getFollowGPS() { return followGPS; } private void toggleShowGPS() { boolean newState = !getShowGPS(); setShowGPS(newState); } private void toggleFollowGPS() { boolean newState = !followGPS; setFollowGPS(newState); map.setFollowGPS(newState); } private void enableLocationUpdates() { //noinspection PointlessBooleanExpression if (wantLocationUpdates == true) return; if (sensorManager != null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) { sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_UI); sensorManager.registerListener(sensorListener, magnetometer, SensorManager.SENSOR_DELAY_UI); } else { sensorManager.registerListener(sensorListener, rotation, SensorManager.SENSOR_DELAY_UI); } } wantLocationUpdates = true; if (getTracker() != null) getTracker().setListenerNeedsGPS(true); } private void disableLocationUpdates() { //noinspection PointlessBooleanExpression if (wantLocationUpdates == false) return; if (sensorManager != null) sensorManager.unregisterListener(sensorListener); wantLocationUpdates = false; if (getTracker() != null) getTracker().setListenerNeedsGPS(false); } /** * Handles the menu click on "download current view".<br> * When no {@link #delegator} is set, the user will be redirected to AreaPicker.<br> * When the user made some changes, {@link #DIALOG_TRANSFER_DOWNLOAD_CURRENT_WITH_CHANGES} will be shown.<br> * Otherwise the current viewBox will be re-downloaded from the server. * @param add Boolean flag indicating to handle changes (true) or not (false). */ private void onMenuDownloadCurrent(boolean add) { Log.d(DEBUG_TAG, "onMenuDownloadCurrent"); if (App.getLogic().hasChanges() && !add) { DownloadCurrentWithChanges.showDialog(this); } else { performCurrentViewHttpLoad(this, add); } } /** * {@inheritDoc} */ @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { Log.d(DEBUG_TAG, "onActivityResult"); super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_BOUNDING_BOX && data != null) { handleBoxPickerResult(resultCode, data); } else if (requestCode == REQUEST_EDIT_TAG && resultCode == RESULT_OK && data != null) { handlePropertyEditorResult(data); } else if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK ) { // reindexPhotos(); if (imageFile != null) { PhotoIndex pi = new PhotoIndex(this); pi.addPhoto(imageFile); if (prefs.isPhotoLayerEnabled()) { map.invalidate(); } } else { Log.e(DEBUG_TAG,"imageFile == null"); } } else if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) { if (easyEditManager.isProcessingAction()) { easyEditManager.handleActivityResult(requestCode, resultCode, data); } else { (new Commands(this)).processIntentResult(data,locationForIntent); locationForIntent = null; map.invalidate(); } } else if ((requestCode == SelectFile.READ_FILE || requestCode == SelectFile.READ_FILE_OLD || requestCode == SelectFile.SAVE_FILE) && resultCode == RESULT_OK) { SelectFile.handleResult(requestCode, data); } scheduleAutoLock(); } /** * @param resultCode The integer result code returned by the child activity * through its setResult(). * @param data An Intent, which can return result data to the caller * (various data can be attached to Intent "extras"). */ private void handleBoxPickerResult(final int resultCode, final Intent data) { Bundle b = data.getExtras(); int left = b.getInt(BoxPicker.RESULT_LEFT); int bottom = b.getInt(BoxPicker.RESULT_BOTTOM); int right = b.getInt(BoxPicker.RESULT_RIGHT); int top = b.getInt(BoxPicker.RESULT_TOP); try { BoundingBox box = new BoundingBox(left, bottom, right, top); if (resultCode == RESULT_OK) { performHttpLoad(box); } else if (resultCode == RESULT_CANCELED) { // synchronized (setViewBoxLock) { setViewBox = false; // stop setting the view box in onResume Log.d(DEBUG_TAG,"opening empty map on " + (box != null ? box.toString() : " null bbox")); openEmptyMap(box); // we may have a valid box } } } catch (OsmException e) { //Values should be done checked in LocationPicker. Log.e(DEBUG_TAG, "OsmException", e); } } /** * Handle the result of the property editor * @param data An Intent, which can return result data to the caller * (various data can be attached to Intent "extras"). */ private void handlePropertyEditorResult(final Intent data) { final Logic logic = App.getLogic(); Bundle b = data.getExtras(); if (b != null && b.containsKey(PropertyEditor.TAGEDIT_DATA)) { // Read data from extras PropertyEditorData[] result = PropertyEditorData.deserializeArray(b.getSerializable(PropertyEditor.TAGEDIT_DATA)); // FIXME Problem saved data may not be read at this point, load here, probably we should load editing state too synchronized (loadOnResumeLock) { if (loadOnResume) { loadOnResume = false; Log.d(DEBUG_TAG,"handlePropertyEditorResult loading data"); logic.syncLoadFromFile(this); // sync load App.getTaskStorage().readFromFile(this); } } for (PropertyEditorData editorData:result) { if (editorData == null) { Log.d(DEBUG_TAG,"handlePropertyEditorResult null result"); continue; } if (editorData.tags != null) { Log.d(DEBUG_TAG,"handlePropertyEditorResult setting tags"); try { logic.setTags(this, editorData.type, editorData.osmId, editorData.tags); } catch (OsmIllegalOperationException e) { Snack.barError(this, e.getMessage()); } } if (editorData.parents != null) { Log.d(DEBUG_TAG,"handlePropertyEditorResult setting parents"); logic.updateParentRelations(this, editorData.type, editorData.osmId, editorData.parents); } if (editorData.members != null && editorData.type.equals(Relation.NAME)) { Log.d(DEBUG_TAG,"handlePropertyEditorResult setting members"); logic.updateRelation(this, editorData.osmId, editorData.members); } } // this is very expensive: getLogic().saveAsync(); // if nothing was changed the dirty flag wont be set and the save wont actually happen } if ((logic.getMode().elementsGeomEditiable() && easyEditManager != null && !easyEditManager.isProcessingAction()) || logic.getMode()==Mode.MODE_TAG_EDIT) { // not in an easy edit mode, de-select objects avoids inconsistent visual state logic.deselectAll(); } else { // invalidate the action mode menu ... updates the state of the undo button // for visual feedback reasons we leave selected elements selected (tag edit mode) supportInvalidateOptionsMenu(); if (easyEditManager != null) { easyEditManager.invalidate(); } } map.invalidate(); } /** * Restore the file name for a photograph * @param savedImageFileName Image file name. */ public void setImageFileName(String savedImageFileName) { if (savedImageFileName != null) { Log.d(DEBUG_TAG, "setting imageFIleName to " + savedImageFileName); imageFile = new File(savedImageFileName); } } /** * Return the file name for a photograph * @return Image file name. */ public String getImageFileName() { if (imageFile != null) { return imageFile.getAbsolutePath(); } return null; } @Override public void onLowMemory() { Log.d(DEBUG_TAG, "onLowMemory"); super.onLowMemory(); map.onLowMemory(); } /** * TODO: put this in Logic!!! Checks if a serialized {@link StorageDelegator} file is available. * * @return true, when the file is available, otherwise false. */ private boolean isLastActivityAvailable() { FileInputStream in = null; try { in = openFileInput(StorageDelegator.FILENAME); return true; } catch (final FileNotFoundException e) { return false; } finally { SavingHelper.close(in); } } public static void performCurrentViewHttpLoad(final Main main, boolean add) { App.getLogic().downloadCurrent(main, add); Preferences prefs = main.prefs; if (prefs.areBugsEnabled()) { // always adds bugs for now final Map map = main.getMap(); TransferTasks.downloadBox(main, prefs.getServer(), map.getViewBox().copy(), true, new PostAsyncActionHandler() { private static final long serialVersionUID = 1L; @Override public void onSuccess() { map.invalidate(); } @Override public void onError() { } }); } } private void performHttpLoad(final BoundingBox box) { App.getLogic().downloadBox(this, box, false, null); } private void openEmptyMap(final BoundingBox box) { App.getLogic().newEmptyMap(this, box); } /** * @param comment Textual comment associated with the change set. * @param source Source of the change. * @param closeChangeset Boolean flag indicating whether the change set * should be closed or kept open. */ public void performUpload(final String comment, final String source, final boolean closeChangeset) { final Logic logic = App.getLogic(); final Server server = prefs.getServer(); if (server != null && server.isLoginSet()) { boolean hasDataChanges = logic.hasChanges(); boolean hasBugChanges = !App.getTaskStorage().isEmpty() && App.getTaskStorage().hasChanges(); if (hasDataChanges || hasBugChanges) { if (hasDataChanges) { logic.upload(this, comment, source, closeChangeset); } if (hasBugChanges) { TransferTasks.upload(this, server, null); } logic.checkForMail(this); } else { Snack.barInfo(this, R.string.toast_no_changes); } } else { ErrorAlert.showDialog(this,ErrorCodes.NO_LOGIN_DATA); } } /** * */ public void performTrackUpload(final String description, final String tags, final Visibility visibility) { final Logic logic = App.getLogic(); final Server server = prefs.getServer(); if (server != null && server.isLoginSet()) { logic.uploadTrack(this, getTracker().getTrack(), description, tags, visibility); logic.checkForMail(this); } else { ErrorAlert.showDialog(this,ErrorCodes.NO_LOGIN_DATA); } } /** * */ public void confirmUpload() { final Server server = prefs.getServer(); if (server != null && server.isLoginSet()) { if (App.getLogic().hasChanges()) { if (server.needOAuthHandshake()) { oAuthHandshake(server, new PostAsyncActionHandler() { private static final long serialVersionUID = 1L; @Override public void onSuccess() { ConfirmUpload.showDialog(Main.this); } @Override public void onError() { } }); if (server.getOAuth()) // if still set Snack.barError(this, R.string.toast_oauth); return; } ConfirmUpload.showDialog(Main.this); } else { Snack.barInfo(this, R.string.toast_no_changes); } } else { ErrorAlert.showDialog(this,ErrorCodes.NO_LOGIN_DATA); } } public void hideBottomBar() { ActionMenuView bottomToolbar = getBottomBar(); if (bottomToolbar != null) { bottomToolbar.setVisibility(View.GONE); } } public void showBottomBar() { ActionMenuView bottomToolbar = getBottomBar(); if (bottomToolbar != null) { bottomToolbar.setVisibility(View.VISIBLE); } } public void hideLock() { FloatingActionButton lock = getLock(); if (lock != null) { lock.hide(); } } public void showLock() { FloatingActionButton lock = getLock(); if (lock != null) { lock.show(); } } private void hideControls() { ActionBar actionbar = getSupportActionBar(); if (actionbar != null) { actionbar.hide(); } hideBottomBar(); hideLock(); ZoomControls zoomControls = getControls(); if (zoomControls != null) { zoomControls.hide(); } hideFollowButton(); if (App.getLogic().getFilter() != null) { App.getLogic().getFilter().hideControls(); } } private void showControls() { ActionBar actionbar = getSupportActionBar(); if (actionbar != null && !prefs.splitActionBarEnabled()) { actionbar.show(); } showBottomBar(); showLock(); ZoomControls zoomControls = getControls(); if (zoomControls != null) { zoomControls.show(); } showFollowButton(); if (App.getLogic().getFilter() != null) { App.getLogic().getFilter().showControls(); } } /** * @param server Server properties. * @param restart Handler to be executed after asynchronous action have been performed. */ @SuppressLint({ "SetJavaScriptEnabled", "InlinedApi", "NewApi" }) public void oAuthHandshake(Server server, PostAsyncActionHandler restart) { descheduleAutoLock(); this.restart = restart; hideControls(); String url = Server.getBaseUrl(server.getReadWriteUrl()); OAuthHelper oa; try { oa = new OAuthHelper(url); } catch (OsmException oe) { server.setOAuth(false); // ups something went wrong turn oauth off showControls(); Snack.barError(this, R.string.toast_no_oauth); return; } Log.d(DEBUG_TAG, "oauth auth url " + url); String authUrl = null; String errorMessage = null; try { authUrl = oa.getRequestToken(); } catch (OAuthException e) { errorMessage = OAuthHelper.getErrorMessage(this, e); } catch (InterruptedException e) { errorMessage = getString(R.string.toast_oauth_communication); } catch (ExecutionException e) { errorMessage = getString(R.string.toast_oauth_communication); } catch (TimeoutException e) { errorMessage = getString(R.string.toast_oauth_timeout); } if (authUrl == null) { Snack.barError(this, errorMessage); showControls(); return; } Log.d(DEBUG_TAG, "authURl " + authUrl); oAuthWebView = new WebView(this); mapLayout.addView(oAuthWebView); oAuthWebView.getSettings().setJavaScriptEnabled(true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { oAuthWebView.getSettings().setAllowContentAccess(true); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { oAuthWebView.getLayoutParams().height = android.view.ViewGroup.LayoutParams.MATCH_PARENT; oAuthWebView.getLayoutParams().width = android.view.ViewGroup.LayoutParams.MATCH_PARENT; } oAuthWebView.requestFocus(View.FOCUS_DOWN); class MyWebViewClient extends WebViewClient { Object progressLock = new Object(); boolean progressShown = false; Runnable dismiss = new Runnable() { @Override public void run() { Progress.dismissDialog(Main.this, Progress.PROGRESS_OAUTH); } }; @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!url.contains("vespucci")) { // load in in this webview view.loadUrl(url); return true; } // vespucci URL Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent); return true; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon){ synchronized(progressLock) { if (!progressShown) { progressShown = true; Progress.showDialog(Main.this, Progress.PROGRESS_OAUTH); } } } @Override public void onPageFinished(WebView view, String url){ synchronized(progressLock) { if (progressShown) { oAuthWebView.removeCallbacks(dismiss); oAuthWebView.postDelayed(dismiss, 500); } } } } oAuthWebView.setWebViewClient(new MyWebViewClient()); oAuthWebView.loadUrl(authUrl); } /** * Remove the OAuth webview */ public void finishOAuth() { Log.d(DEBUG_TAG,"finishOAuth"); if (oAuthWebView != null) { mapLayout.removeView(oAuthWebView); showControls(); try { // the below loadUrl, even though the "official" way to do it, // seems to be prone to crash on some devices. oAuthWebView.loadUrl("about:blank"); // workaround clearView issues oAuthWebView.setVisibility(View.GONE); oAuthWebView.removeAllViews(); oAuthWebView.destroy(); oAuthWebView = null; if (restart != null) { restart.onSuccess(); } } catch (Exception ex) { ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(ex); } } } /** * Starts the LocationPicker activity for requesting a location. */ public void gotoBoxPicker() { descheduleAutoLock(); Intent intent = new Intent(this, BoxPicker.class); if (App.getLogic().hasChanges()) { DataLossActivity.showDialog(this, intent, REQUEST_BOUNDING_BOX); } else { startActivityForResult(intent, REQUEST_BOUNDING_BOX); } } /** * Start the PropertyEditor for the element in question, single element version * @param selectedElement Selected OpenStreetMap element. * @param focusOn if not null focus on the value field of this key. * @param applyLastAddressTags add address tags to the object being edited. * @param showPresets show the preset tab on start up. * @param askForName ask for a value for the name tag */ public void performTagEdit(final OsmElement selectedElement, String focusOn, boolean applyLastAddressTags, boolean showPresets, boolean askForName) { descheduleAutoLock(); final Logic logic = App.getLogic(); logic.deselectAll(); if (selectedElement instanceof Node) { logic.setSelectedNode((Node) selectedElement); } else if (selectedElement instanceof Way) { logic.setSelectedWay((Way) selectedElement); } else if (selectedElement instanceof Relation) { logic.setSelectedRelation((Relation) selectedElement); } if (selectedElement != null) { StorageDelegator storageDelegator = App.getDelegator(); if (storageDelegator.getOsmElement(selectedElement.getName(), selectedElement.getOsmId()) != null) { PropertyEditorData[] single = new PropertyEditorData[1]; single[0] = new PropertyEditorData(selectedElement, focusOn); PropertyEditor.startForResult(this, single, applyLastAddressTags, showPresets, askForName, logic.getMode().getExtraTags(logic, selectedElement), REQUEST_EDIT_TAG); } } } /** * Start the PropertyEditor for the element in question, multiple element version * @param selection list of selected elements * @param applyLastAddressTags add address tags to the object being edited. * @param showPresets show the preset tab on start up. */ public void performTagEdit(final ArrayList<OsmElement> selection, boolean applyLastAddressTags, boolean showPresets) { descheduleAutoLock(); ArrayList<PropertyEditorData> multiple = new ArrayList<PropertyEditorData>(); StorageDelegator storageDelegator = App.getDelegator(); for (OsmElement e:selection) { if (storageDelegator.getOsmElement(e.getName(), e.getOsmId()) != null) { multiple.add(new PropertyEditorData(e, null)); } } if (multiple.isEmpty()) { Log.d(DEBUG_TAG, "performTagEdit no valid elements"); return; } PropertyEditorData[] multipleArray = multiple.toArray(new PropertyEditorData[multiple.size()]); PropertyEditor.startForResult(this, multipleArray, applyLastAddressTags, showPresets, false, null, REQUEST_EDIT_TAG); } /** * Edit an OpenStreetBug. * @param bug The bug to edit. */ private void performBugEdit(final Task bug) { Log.d(DEBUG_TAG, "editing bug:"+bug); descheduleAutoLock(); App.getLogic().setSelectedBug(bug); FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); Fragment prev = fm.findFragmentByTag("fragment_bug"); try { if (prev != null) { ft.remove(prev); } ft.commit(); } catch (IllegalStateException isex) { Log.e(DEBUG_TAG,"performBugEdit removing dialog ",isex); } TaskFragment bugDialog = TaskFragment.newInstance(bug); try { bugDialog.show(fm, "fragment_bug"); } catch (IllegalStateException isex) { // FIXME properly Log.e(DEBUG_TAG,"performBugEdit showing dialog ",isex); } } /** * potentially do some special stuff for invoking undo and exiting */ @Override public void onBackPressed() { // super.onBackPressed(); Log.d(DEBUG_TAG,"onBackPressed()"); if (oAuthWebView != null && oAuthWebView.canGoBack()) { // we are displaying the oAuthWebView and somebody might want to navigate back oAuthWebView.goBack(); return; } if (prefs.useBackForUndo()) { String name = App.getLogic().undo(); if (name != null) { Snack.barInfo(this, getResources().getString(R.string.undo) + ": " + name); } else { exitOnBackPressed(); } } else { exitOnBackPressed(); } } /** * pop up a dialog asking for confirmation and exit */ private void exitOnBackPressed() { new AlertDialog.Builder(this) .setTitle(R.string.exit_title) .setMessage(R.string.exit_text) .setNegativeButton(R.string.no, null) .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface arg0, int arg1) { // if we actually exit, stop the auto downloads, for now allow GPS tracks to carry on if (getTracker() != null) { getTracker().stopAutoDownload(); getTracker().stopBugAutoDownload(); } try { saveSync = true; Main.super.onBackPressed(); } catch (Exception e) { // silently ignore .. might be Android confusion } } }).create().show(); } private boolean actionResult = false; /** * catch back button in action modes where onBackPressed is not invoked * this is probably not guaranteed to work */ @Override public boolean dispatchKeyEvent(KeyEvent event) { if(easyEditManager != null && easyEditManager.isProcessingAction()) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (event.getAction() == KeyEvent.ACTION_DOWN) { Log.d(DEBUG_TAG,"calling handleBackPressed"); actionResult = easyEditManager.handleBackPressed(); return actionResult; } else { // note to avoid tons of error messages we need to consume both events return actionResult; } } } return super.dispatchKeyEvent(event); } public class UndoListener implements OnClickListener, OnLongClickListener { private final String DEBUG_TAG = UndoListener.class.getName(); @Override public void onClick(View arg0) { Log.d(DEBUG_TAG,"normal click"); final Logic logic = App.getLogic(); String name = logic.undo(); if (name != null) { Snack.barInfoShort(Main.this, getResources().getString(R.string.undo) + ": " + name); } else { Snack.barInfoShort(Main.this, R.string.undo_nothing); } resync(logic); map.invalidate(); } @Override public boolean onLongClick(View v) { Log.d(DEBUG_TAG,"long click"); final Logic logic = App.getLogic(); UndoStorage undo = logic.getUndo(); if (undo.canUndo() || undo.canRedo()) { UndoDialogFactory.showUndoDialog(Main.this, logic, undo); } else { Snack.barInfoShort(Main.this, R.string.undo_nothing); } resync(logic); map.invalidate(); return true; } void resync (final Logic logic) { // check that we haven't just removed a selected element if (logic.resyncSelected()) { // only need to test if anything at all is still selected if (logic.selectedNodesCount() + logic.selectedWaysCount() + logic.selectedRelationsCount() == 0 ) { easyEditManager.finish(); } } } } /** * A TouchListener for all gestures made on the touchscreen. * * @author mb */ private class MapTouchListener implements OnTouchListener, VersionedGestureDetector.OnGestureListener, OnCreateContextMenuListener, OnMenuItemClickListener { private List<OsmElement> clickedNodesAndWays; private List<Task> clickedBugs; private List<Photo> clickedPhotos; private boolean doubleTap = false; @Override public boolean onTouch(final View v, final MotionEvent m) { descheduleAutoLock(); // Log.d("MapTouchListener", "onTouch"); if (m.getAction() == MotionEvent.ACTION_DOWN) { // Log.d("MapTouchListener", "onTouch ACTION_DOWN"); clickedBugs = null; clickedPhotos = null; clickedNodesAndWays = null; App.getLogic().handleTouchEventDown(Main.this,m.getX(), m.getY()); } if (m.getAction() == MotionEvent.ACTION_UP) { App.getLogic().handleTouchEventUp(m.getX(), m.getY()); scheduleAutoLock(); } mDetector.onTouchEvent(v, m); return v.onTouchEvent(m); } @Override public void onDown(View v, float x, float y) {} @Override public void onClick(View v, float x, float y) { de.blau.android.tasks.MapOverlay osbo = map.getOpenStreetBugsOverlay(); clickedBugs = (osbo != null) ? osbo.getClickedTasks(x, y, map.getViewBox()) : null; de.blau.android.photos.MapOverlay photos = map.getPhotosOverlay(); clickedPhotos = (photos != null) ? photos.getClickedPhotos(x, y, map.getViewBox()) : null; final Logic logic = App.getLogic(); Mode mode = logic.getMode(); boolean isInEditZoomRange = logic.isInEditZoomRange(); if (isInEditZoomRange) { if (logic.isLocked()) { if (NetworkStatus.isConnected(Main.this) && prefs.voiceCommandsEnabled()) { locationForIntent = lastLocation; // location when we touched the screen startVoiceRecognition(); } else { Snack.barInfoShort(Main.this, R.string.toast_unlock_to_edit); } } else { if (mode.elementsEditable()) { performEdit(mode, v, x, y); } } map.invalidate(); } else { switch (((clickedBugs == null) ? 0 : clickedBugs.size()) + ((clickedPhotos == null) ? 0 : clickedPhotos.size())) { case 0: if (!isInEditZoomRange && !logic.isLocked()) { Snack.barInfoShort(v, R.string.toast_not_in_edit_range); } break; case 1: if ((clickedBugs != null) && (clickedBugs.size() > 0)) performBugEdit(clickedBugs.get(0)); else if ((clickedPhotos != null) && (clickedPhotos.size() > 0)) viewPhoto(clickedPhotos.get(0)); break; default: v.showContextMenu(); break; } } } @SuppressLint("InlinedApi") private void viewPhoto(Photo photo) { try { Intent myIntent = new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT|Intent.FLAG_GRANT_READ_URI_PERMISSION); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_GRANT_READ_URI_PERMISSION); } Uri photoUri = photo.getRef(Main.this); if (photoUri != null) { myIntent.setDataAndType(photoUri, "image/jpeg"); // black magic only works this way startActivity(myIntent); map.getPhotosOverlay().setSelected(photo); //TODO may need a map.invalidate() here } else { Snack.barError(Main.this, Main.this.getResources().getString(R.string.toast_error_accessing_photo, photo.getRef())); } } catch (Exception ex) { Log.d(DEBUG_TAG, "viewPhoto exception starting intent: " + ex); ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH"); ACRA.getErrorReporter().handleException(ex); } } @Override public void onUp(View v, float x, float y) { if (App.getLogic().getMode().elementsGeomEditiable()) { easyEditManager.invalidate(); } } @Override public boolean onLongClick(final View v, final float x, final float y) { final Logic logic = App.getLogic(); if (logic.isLocked()) { if (logic.getMode().elementsGeomEditiable()) { // display context menu de.blau.android.tasks.MapOverlay osbo = map.getOpenStreetBugsOverlay(); clickedBugs = (osbo != null) ? osbo.getClickedTasks(x, y, map.getViewBox()) : null; de.blau.android.photos.MapOverlay photos = map.getPhotosOverlay(); clickedPhotos = (photos != null) ? photos.getClickedPhotos(x, y, map.getViewBox()) : null; clickedNodesAndWays = logic.getClickedNodesAndWays(x, y); int bugCount = clickedBugs != null ? clickedBugs.size() : 0; int photoCount = clickedPhotos != null ? clickedPhotos.size() : 0; int elementCount = clickedNodesAndWays != null ? clickedNodesAndWays.size() : 0; int itemCount = bugCount + photoCount + elementCount; if (itemCount == 1) { if (photoCount==1) { viewPhoto(clickedPhotos.get(0)); } else if (bugCount==1) { performBugEdit(clickedBugs.get(0)); } else if (elementCount==1) { ElementInfo.showDialog(Main.this,clickedNodesAndWays.get(0)); } } else if (itemCount > 0) { v.showContextMenu(); } return true; } else { // other modes return false; // ignore long clicks } } if (logic.isInEditZoomRange()) { setFollowGPS(false); // editing with the screen moving under you is a pain return easyEditManager.handleLongClick(v, x, y); } else { Snack.barWarningShort(Main.this, R.string.toast_not_in_edit_range); } return true; // long click handled } @Override public void onDrag(View v, float x, float y, float dx, float dy) { // Log.d("MapTouchListener", "onDrag dx " + dx + " dy " + dy ); App.getLogic().handleTouchEventMove(Main.this, x, y, -dx, dy); setFollowGPS(false); } @Override public void onScale(View v, float scaleFactor, float prevSpan, float curSpan) { App.getLogic().zoom((curSpan - prevSpan) / prevSpan); updateZoomControls(); } /** * Perform edit touch processing. * @param mode mode we are in, either EASYEDIT or TAG_EDIT * @param v View affected by the touch event. * @param x the click-position on the display. * @param y the click-position on the display. */ public void performEdit(Mode mode, final View v, final float x, final float y) { if (!easyEditManager.actionModeHandledClick(x, y)) { clickedNodesAndWays = App.getLogic().getClickedNodesAndWays(x, y); Logic logic = App.getLogic(); Filter filter = logic.getFilter(); if (filter != null) { // filter elements clickedNodesAndWays = filterElements(clickedNodesAndWays); } boolean inEasyEditMode = logic.getMode().elementsGeomEditiable(); switch (((clickedBugs == null) ? 0 : clickedBugs.size()) + clickedNodesAndWays.size() + ((clickedPhotos == null)? 0 : clickedPhotos.size())) { case 0: // no elements were touched if (inEasyEditMode) { easyEditManager.nothingTouched(false); } break; case 1: // exactly one element touched if (clickedBugs != null && clickedBugs.size() == 1) { performBugEdit(clickedBugs.get(0)); } else if (clickedPhotos != null && clickedPhotos.size() == 1) { viewPhoto(clickedPhotos.get(0)); } else { if (inEasyEditMode) { easyEditManager.editElement(clickedNodesAndWays.get(0)); } else { performTagEdit(clickedNodesAndWays.get(0), null, false, false, false); } } break; default: // multiple possible elements touched - show menu if (menuRequired()) { v.showContextMenu(); } else { // menuRequired tells us it's ok to just take the first one if (inEasyEditMode) { easyEditManager.editElement(clickedNodesAndWays.get(0)); } else { performTagEdit(clickedNodesAndWays.get(0), null, false, false, false); } } break; } } } /** * Filter for elements, NOTE expensive for a large number of elements * @param elements * @return List of elements that are on the current level */ private ArrayList<OsmElement> filterElements(List<OsmElement> elements) { ArrayList<OsmElement>tmp = new ArrayList<OsmElement>(); Logic logic = App.getLogic(); Filter filter = logic.getFilter(); for (OsmElement e:elements) { if (filter.include(e, false)) { tmp.add(e); } } return tmp; } @Override public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) { if (easyEditManager.needsCustomContextMenu()) { easyEditManager.createContextMenu(menu); } else { onCreateDefaultContextMenu(menu); } } /** * Creates a context menu with the objects near where the screen was touched * @param menu */ public void onCreateDefaultContextMenu(final ContextMenu menu) { int id = 0; if (clickedPhotos != null) { for (Photo p : clickedPhotos) { Uri photoUri = p.getRef(Main.this); if (photoUri != null) { menu.add(Menu.NONE, id++, Menu.NONE, photoUri.getLastPathSegment()).setOnMenuItemClickListener(this); } } } if (clickedBugs != null) { for (Task b : clickedBugs) { menu.add(Menu.NONE, id++, Menu.NONE, b.getDescription()).setOnMenuItemClickListener(this); } } if (clickedNodesAndWays != null) { Logic logic = App.getLogic(); for (OsmElement e : clickedNodesAndWays) { String description = e.getDescription(Main.this); if (e instanceof Node) { List<Way> ways = App.getLogic().getWaysForNode((Node)e); if (ways != null && ways.size() > 0) { description = description + " ("; for (Way w:ways) { description = description + w.getDescription(Main.this) + ((ways.indexOf(w)!=(ways.size()-1)?", ":"")); } description = description + ")"; } } if (logic.isSelected(e)) { SpannableString s = new SpannableString(description); s.setSpan(new ForegroundColorSpan(ThemeUtils.getStyleAttribColorValue(Main.this, R.attr.colorAccent, 0)), 0, s.length(), 0); menu.add(Menu.NONE, id++, Menu.NONE, s).setOnMenuItemClickListener(this); } else { menu.add(Menu.NONE, id++, Menu.NONE, description).setOnMenuItemClickListener(this); } } } } /** * Checks if a menu should be shown based on clickedNodesAndWays and clickedBugs. * ClickedNodesAndWays needs to contain nodes first, then ways, ordered by distance from the click. * Assumes multiple elements have been clicked, i.e. a choice is necessary unless heuristics work. */ private boolean menuRequired() { // If the context menu setting requires the menu, show it instead of guessing. if (prefs.getForceContextMenu()) return true; // If bugs are clicked, user should always choose if (clickedBugs != null && clickedBugs.size() > 0) return true; // If photos are clicked, user should always choose if (clickedPhotos != null && clickedPhotos.size() > 0) return true; if (clickedNodesAndWays.size() < 2) { Log.e(DEBUG_TAG, "WTF? menuRequired called for single item?"); return true; } // No bugs were clicked. Do we have nodes? if (clickedNodesAndWays.get(0) instanceof Node) { // Usually, we just take the first node. // However, check for *very* closely overlapping nodes first. Node candidate = (Node) clickedNodesAndWays.get(0); if (candidate.hasParentRelations()) { return true; // otherwise a relation that only has nodes as member is not selectable } final Logic logic = App.getLogic(); float nodeX = logic.getNodeScreenX(candidate); float nodeY = logic.getNodeScreenY(candidate); for (int i = 1; i < clickedNodesAndWays.size(); i++) { if (!(clickedNodesAndWays.get(i) instanceof Node)) break; Node possibleNeighbor = (Node)clickedNodesAndWays.get(i); float node2X = logic.getNodeScreenX(possibleNeighbor); float node2Y = logic.getNodeScreenY(possibleNeighbor); // Fast "square" checking is good enough if (Math.abs(nodeX-node2X) < DataStyle.NODE_OVERLAP_TOLERANCE_VALUE || Math.abs(nodeY-node2Y) < DataStyle.NODE_OVERLAP_TOLERANCE_VALUE ) { // The first node has an EXTREMELY close neighbour. Show context menu return true; } } return false; // no colliding neighbours found } // No nodes means we have at least two ways. Since the tolerance for ways is tiny, show the menu. return true; } @Override public boolean onMenuItemClick(final android.view.MenuItem item) { int itemId = item.getItemId(); int bugsItemId = itemId - ((clickedPhotos == null) ? 0 : clickedPhotos.size()); if ((clickedPhotos != null) && (itemId < clickedPhotos.size())) { viewPhoto(clickedPhotos.get(itemId)); } else if (clickedBugs != null && bugsItemId >= 0 && bugsItemId < clickedBugs.size()) { performBugEdit(clickedBugs.get(bugsItemId)); } else { // this is dependent on which order items where added to the context menu itemId -= (((clickedBugs == null) ? 0 : clickedBugs.size() ) + ((clickedPhotos == null) ? 0 : clickedPhotos.size())); if ((itemId >= 0) && (clickedNodesAndWays != null) && (itemId < clickedNodesAndWays.size())) { final OsmElement element = clickedNodesAndWays.get(itemId); if (App.getLogic().isLocked()) { ElementInfo.showDialog(Main.this,element); } else { Mode mode = App.getLogic().getMode(); if (mode.elementsGeomEditiable()) { if (doubleTap) { doubleTap = false; easyEditManager.startExtendedSelection(element); } else { easyEditManager.editElement(element); } } else if (mode.elementsEditable()) { performTagEdit(element, null, false, false, false); } } } } return true; } @Override public boolean onDoubleTap(View v, float x, float y) { final Logic logic = App.getLogic(); if (!logic.isLocked()) { boolean inEasyEditMode = logic.getMode().elementsGeomEditiable(); clickedNodesAndWays = logic.getClickedNodesAndWays(x, y); switch (clickedNodesAndWays.size()) { case 0: // no elements were touched if (inEasyEditMode) { easyEditManager.nothingTouched(true); // short cut to finishing multi-select } break; case 1: if (inEasyEditMode) { easyEditManager.startExtendedSelection(clickedNodesAndWays.get(0)); } break; default: // multiple possible elements touched - show menu if (inEasyEditMode) { if (menuRequired()) { Log.d(DEBUG_TAG,"onDoubleTap displaying menu"); doubleTap = true; // ugly flag v.showContextMenu(); } else { // menuRequired tells us it's ok to just take the first one easyEditManager.startExtendedSelection(clickedNodesAndWays.get(0)); } } break; } } else { Snack.barInfoShort(Main.this, R.string.toast_unlock_to_edit); } return true; } } /** * A KeyListener for all key events. * * @author mb */ public class MapKeyListener implements OnKeyListener { @SuppressLint("NewApi") @Override public boolean onKey(final View v, final int keyCode, final KeyEvent event) { scheduleAutoLock(); final Logic logic = App.getLogic(); switch (event.getAction()) { case KeyEvent.ACTION_UP: if (!v.onKeyUp(keyCode, event)) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: // this stops the piercing beep related to volume adjustments return true; } } break; case KeyEvent.ACTION_DOWN: if (!v.onKeyDown(keyCode, event)) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: setFollowGPS(true); return true; case KeyEvent.KEYCODE_DPAD_UP: translate(Logic.CursorPaddirection.DIRECTION_UP); return true; case KeyEvent.KEYCODE_DPAD_DOWN: translate(Logic.CursorPaddirection.DIRECTION_DOWN); return true; case KeyEvent.KEYCODE_DPAD_LEFT: translate(Logic.CursorPaddirection.DIRECTION_LEFT); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: translate(Logic.CursorPaddirection.DIRECTION_RIGHT); return true; case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_SEARCH: logic.zoom(Logic.ZOOM_IN); updateZoomControls(); return true; case KeyEvent.KEYCODE_VOLUME_DOWN: logic.zoom(Logic.ZOOM_OUT); updateZoomControls(); return true; default: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { Character c = Character.toLowerCase((char) event.getUnicodeChar()); if (c == Util.getShortCut(Main.this, R.string.shortcut_zoom_in)) { logic.zoom(Logic.ZOOM_IN); updateZoomControls(); return true; } else if (c == Util.getShortCut(Main.this, R.string.shortcut_zoom_out)) { logic.zoom(Logic.ZOOM_OUT); updateZoomControls(); return true; } if (easyEditManager.isProcessingAction() && event.isCtrlPressed()) { // shortcuts not supported in action modes arghhh char shortcut = Character.toLowerCase((char) event.getUnicodeChar(0)); // get rid of Ctrl key if (easyEditManager.processShortcut(shortcut)) { return true; } } } } } break; } return false; } private void translate(final CursorPaddirection direction) { setFollowGPS(false); App.getLogic().translate(direction); } } /** * Mouse scroll wheel support * @author simon * */ @SuppressLint("NewApi") private class MotionEventListener implements OnGenericMotionListener { @SuppressLint("NewApi") @Override public boolean onGenericMotion(View arg0,MotionEvent event) { final Logic logic = App.getLogic(); if (0 != (event.getSource() & InputDevice.SOURCE_CLASS_POINTER)) { switch (event.getAction()) { case MotionEvent.ACTION_SCROLL: if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) { logic.zoom(Logic.ZOOM_IN); } else { logic.zoom(Logic.ZOOM_OUT); } updateZoomControls(); return true; } } return false; } } /** * Invalidates (redraws) the map */ public void invalidateMap() { map.invalidate(); } public Map getMap() { return map; } public static boolean hasChanges() { final Logic logic = App.getLogic(); //noinspection SimplifiableIfStatement if (logic == null) return false; return logic.hasChanges(); } /** * Sets the activity to re-download the last downloaded area on startup * (use e.g. when the API URL is changed) */ public static void prepareRedownload() { redownloadOnResume = true; } @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.i(DEBUG_TAG, "Tracker service connected"); setTracker((((TrackerBinder)service).getService())); map.setTracker(getTracker()); getTracker().setListener(this); getTracker().setListenerNeedsGPS(wantLocationUpdates); startStopAutoDownload(); startStopBugAutoDownload(); triggerMenuInvalidation(); } @Override public void onServiceDisconnected(ComponentName name) { // should never happen, but just to be sure Log.i(DEBUG_TAG, "Tracker service disconnected"); setTracker(null); map.setTracker(null); triggerMenuInvalidation(); } @Override public void onLocationChanged(Location location) { if (followGPS) { BoundingBox viewBox = map.getViewBox(); // ensure the view is zoomed in to at least the most zoomed-out while (!viewBox.canZoomOut() && viewBox.canZoomIn()) { viewBox.zoomIn(); } // re-center on current position viewBox.moveTo(getMap(), (int) (location.getLongitude() * 1E7d), (int) (location.getLatitude() * 1E7d)); } lastLocation = location; if (showGPS) { map.setLocation(location); } map.invalidate(); } @Override public void onStateChanged() { supportInvalidateOptionsMenu(); } /** * Simply calls {@link #invalidateOptionsMenu()}. * MUST BE CALLED FROM THE MAIN/UI THREAD! */ public void triggerMenuInvalidation() { Log.d(DEBUG_TAG, "triggerMenuInvalidation called"); super.supportInvalidateOptionsMenu(); // TODO delay or make conditional to work around android bug? } /** * @return the backgroundAlignmentActionModeCallback */ public BackgroundAlignmentActionModeCallback getBackgroundAlignmentActionModeCallback() { return backgroundAlignmentActionModeCallback; } /** * @return the tracker */ public TrackerService getTracker() { return tracker; } /** * @param tracker the tracker to set */ private void setTracker(TrackerService tracker) { this.tracker = tracker; } public void zoomToAndEdit(int lonE7, int latE7, OsmElement e) { Log.d(DEBUG_TAG,"zoomToAndEdit Zoom " + map.getZoomLevel()); final Logic logic = App.getLogic(); // if (logic.getMode()==Mode.MODE_MOVE) { // avoid switching to the wrong mode // FloatingActionButton lock = setLock(Mode.MODE_MOVE); // NOP to get button // if (EASY_TAG.equals(lock.getTag())) { // setLock(Mode.MODE_EASYEDIT); // } else if (INDOOR_TAG.equals(lock.getTag())) { // setLock(Mode.MODE_INDOOR); // } else{ // setLock(Mode.MODE_TAG_EDIT); // } // } zoomTo(lonE7, latE7, e); logic.setSelectedNode(null); logic.setSelectedWay(null); logic.setSelectedRelation(null); switch (e.getType()) { case NODE: logic.setSelectedNode((Node) e); break; case WAY: case CLOSEDWAY: logic.setSelectedWay((Way) e); break; case RELATION: logic.setSelectedRelation((Relation) e); break; case AREA: if (Way.NAME.equals(e.getName())) { logic.setSelectedWay((Way) e); } else { logic.setSelectedRelation((Relation) e); } break; } if (logic.getMode().elementsGeomEditiable()) { easyEditManager.editElement(e); map.invalidate(); } else { // tag edit mode performTagEdit(e, null, false, false, false); } } /** * Zoom to the coordinates and try and set the viewbox size to something reasonable * @param lonE7 * @param latE7 * @param e */ private void zoomTo(int lonE7, int latE7, OsmElement e) { setFollowGPS(false); // otherwise the screen could move around if (e instanceof Node && map.getZoomLevel() < 22) { App.getLogic().setZoom(getMap(), 22); // FIXME this doesn't seem to work as expected } else { map.getViewBox().setBorders(getMap(), e.getBounds(),false); } map.getViewBox().moveTo(getMap(), lonE7, latE7); } public void zoomTo( OsmElement e) { setFollowGPS(false); // otherwise the screen could move around if (e instanceof Node && map.getZoomLevel() < 22) { App.getLogic().setZoom(getMap(), 22); // FIXME this doesn't seem to work as expected map.getViewBox().moveTo(getMap(), ((Node)e).getLon(), ((Node)e).getLat()); } else { map.getViewBox().setBorders(getMap(), e.getBounds(),false); } } @Override /** * Workaround for bug mentioned below */ public ActionMode startSupportActionMode(@NonNull final ActionMode.Callback callback) { // Fix for bug https://code.google.com/p/android/issues/detail?id=159527 final ActionMode mode = super.startSupportActionMode(callback); if (mode != null) { mode.invalidate(); } return mode; } @Override // currently this is only called by the task UI public void update() { if (App.getLogic().getSelectedBug() != null) { App.getLogic().setSelectedBug(null); } map.invalidate(); } /** * @return the bottomToolbar */ public android.support.v7.widget.ActionMenuView getBottomBar() { return bottomBar; } /** * @param bottomBar the bottomToolbar to set */ private void setBottomBar(android.support.v7.widget.ActionMenuView bottomBar) { MenuUtil.setupBottomBar(this, bottomBar, isFullScreen(), prefs.lightThemeEnabled()); this.bottomBar = bottomBar; } /** * @return the view containing the zoom + and - buttons */ private ZoomControls getControls() { return zoomControls; } /** * @return the "center on GPS position button" */ private FloatingActionButton getFollowButton() { return follow; } /** * Display the "center on GPS position" button */ private void hideFollowButton() { FloatingActionButton follow = getFollowButton(); if (follow != null) { follow.hide(); } } /** * Display the "center on GPS position" button, checks if GPS is actually on */ private void showFollowButton() { FloatingActionButton follow = getFollowButton(); if (follow != null && ensureGPSProviderEnabled() && locationPermissionGranted && !"NONE".equals(prefs.followGPSbuttonPosition())) { follow.show(); } } /** * Lock screen if we are in a mode in which that can reasonably be done */ private Runnable autoLock = new Runnable() { @Override public void run() { if (!App.getLogic().isLocked()) { if (!easyEditManager.isProcessingAction() || easyEditManager.inElementSelectedMode()) { View lock = getLock(); if (lock != null) { lock.performClick(); } if (easyEditManager.inElementSelectedMode()) { App.getLogic().deselectAll(); easyEditManager.finish(); } } else { // can't lock now, reschedule if (prefs != null) { int delay = prefs.getAutolockDelay(); if (delay > 0) { map.postDelayed(autoLock, delay); } } } } } }; /** * Schedule automatic locking of the screen in a configurable time in the future */ void scheduleAutoLock() { map.removeCallbacks(autoLock); if (prefs != null) { int delay = prefs.getAutolockDelay(); if (delay > 0) { map.postDelayed(autoLock, delay); } } } /** * Remove any pending automatic lock tasks */ private void descheduleAutoLock() { map.removeCallbacks(autoLock); } public RelativeLayout getMapLayout() { return mapLayout; } }