Java tutorial
// Copyright (C) 2013 City of Copenhagen. // // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // http://mozilla.org/MPL/2.0/. package dk.kk.ibikecphlib.map; import android.Manifest; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.location.Location; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.widget.DrawerLayout; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ImageButton; import com.balysv.materialmenu.MaterialMenuIcon; import com.balysv.materialmenu.MaterialMenuDrawable; import com.google.android.gms.location.LocationListener; import com.mapbox.mapboxsdk.events.MapListener; import com.mapbox.mapboxsdk.events.RotateEvent; import com.mapbox.mapboxsdk.events.ScrollEvent; import com.mapbox.mapboxsdk.events.ZoomEvent; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.overlay.Overlay; import com.mapbox.mapboxsdk.overlay.UserLocationOverlay; import dk.kk.ibikecphlib.IBikeApplication; import dk.kk.ibikecphlib.LeftMenu; import dk.kk.ibikecphlib.R; import dk.kk.ibikecphlib.TermsManager; import dk.kk.ibikecphlib.favorites.FavoriteListItem; import dk.kk.ibikecphlib.login.LoginActivity; import dk.kk.ibikecphlib.login.ProfileActivity; import dk.kk.ibikecphlib.map.fragments.BreakRouteSelectionFragment; import dk.kk.ibikecphlib.map.fragments.RouteSelectionFragment; import dk.kk.ibikecphlib.map.handlers.OverviewMapHandler; import dk.kk.ibikecphlib.map.overlays.TogglableOverlay; import dk.kk.ibikecphlib.map.overlays.TogglableOverlayFactory; import dk.kk.ibikecphlib.map.states.BrowsingState; import dk.kk.ibikecphlib.map.states.DestinationPreviewState; import dk.kk.ibikecphlib.map.states.MapState; import dk.kk.ibikecphlib.map.states.NavigatingState; import dk.kk.ibikecphlib.map.states.RouteSelectionState; import dk.kk.ibikecphlib.search.Address; import dk.kk.ibikecphlib.search.SearchActivity; import dk.kk.ibikecphlib.search.SearchAutocompleteActivity; import dk.kk.ibikecphlib.util.Util; import dk.kk.ibikecphlib.util.LOG; import java.util.List; /** * The main map view. * <p/> * TODO: Look into ways of making this class shorter. * * @author jens */ @SuppressLint("NewApi") public class MapActivity extends BaseMapActivity { public final static int REQUEST_SEARCH_ADDRESS = 2; public final static int REQUEST_CHANGE_SOURCE_ADDRESS = 250; public final static int REQUEST_CHANGE_DESTINATION_ADDRESS = 251; public final static int RESULT_RETURN_FROM_NAVIGATION = 105; public final static int PERMISSIONS_REQUEST_FINE_LOCATION = 1; protected MapState state; public static Context mapActivityContext; // This is used to throttle calls to setting image resource on the compas protected UserLocationOverlay.TrackingMode previousTrackingMode; protected LeftMenu leftMenu; private DrawerLayout drawerLayout; private MaterialMenuIcon materialMenu; protected IBCMapView mapView; /** * @deprecated Let's phase out the use of static members like this. */ public static View topFragment; /** * @deprecated Let's phase out the use of static members like this. */ public static boolean fromSearch = false; /** * @deprecated Let's phase out the use of static booleans like this. */ public static boolean isBreakChosen = false; private static final String TAG = "MapActivity"; protected LocationListener locationListener; @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Link the activity to the map activity layout. setContentView(R.layout.map_activity); // Make sure the app icon is clickable getActionBar().setHomeButtonEnabled(true); getActionBar().setDisplayHomeAsUpEnabled(true); getActionBar().setDisplayShowCustomEnabled(true); // Create the drawer menu to the left. createMenu(); // TODO: Remove this after reimplementing the logout* methods of the IBikeApplication class mapActivityContext = this; // Finding the sub-components of the activity's view, consider if these need to be static // or if we could pass a reference to this activity to the components that needs access topFragment = findViewById(R.id.topFragment); // Initialize the map view mapView = (IBCMapView) findViewById(R.id.mapView); mapView.init(this); changeState(BrowsingState.class); initializeSelectableOverlays(); // Check if the user accepts the newest terms // TermsManager.checkTerms(this); // When scrolling the map, make sure that the compass icon is updated. this.mapView.addListener(new MapListener() { @Override public void onScroll(ScrollEvent scrollEvent) { updateCompassIcon(); } @Override public void onZoom(ZoomEvent zoomEvent) { updateCompassIcon(); } @Override public void onRotate(RotateEvent rotateEvent) { updateCompassIcon(); } }); } protected void initializeSelectableOverlays() { final TogglableOverlayFactory factory = TogglableOverlayFactory.getInstance(); factory.addOnOverlaysLoadedListener(new TogglableOverlayFactory.OnOverlaysLoadedListener() { @Override public void onOverlaysLoaded(List<TogglableOverlay> togglableOverlays) { // Add all the overlays to the map view for (TogglableOverlay togglableOverlay : factory.getTogglableOverlays()) { for (Overlay overlay : togglableOverlay.getOverlays()) { mapView.addOverlay(overlay); } } } }); } @SuppressWarnings("deprecation") @Override public void onResume() { super.onResume(); attemptToRegisterLocationListener(); fromSearch = false; if (!Util.isNetworkConnected(this)) { Util.launchNoConnectionDialog(this); } // TODO: Check if this is even needed as the menu has been added using the fragment manager. leftMenu.onResume(); // Check if the user accepts the newest terms TermsManager.checkTerms(this); // Check if the user was logged out/deleted and spawn a dialog Intent intent = getIntent(); if (intent.hasExtra("loggedOut")) { if (intent.getExtras().getBoolean("loggedOut")) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(IBikeApplication.getString("invalid_token_user_logged_out")); builder.setPositiveButton(IBikeApplication.getString("log_in"), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent intent = new Intent(MapActivity.this, LoginActivity.class); startActivity(intent); dialog.dismiss(); } }); builder.setNegativeButton(IBikeApplication.getString("close"), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }); builder.setCancelable(false); builder.show(); } intent.removeExtra("loggedOut"); } else if (intent.hasExtra("deleteUser")) { if (intent.getExtras().getBoolean("deleteUser")) { AlertDialog.Builder builder = new AlertDialog.Builder(MapActivity.this); builder.setMessage(IBikeApplication.getString("account_deleted")); builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); AlertDialog dialog = builder.create(); dialog.show(); } intent.removeExtra("deleteUser"); } } /** * Transition the map activity to another state. * * @param toState the new state */ public void changeState(MapState toState) { FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); MapState fromState = state; String logMessage = "Changed state "; // Give the state a reference back to this activity. toState.setMapActivity(this); // Transition away from the current state. if (fromState != null) { logMessage += String.format("from %s ", fromState); fromState.transitionAway(toState, fragmentTransaction); } // Transition to the new state state = toState; logMessage += String.format("to %s", toState); toState.transitionTowards(fromState, fragmentTransaction); fragmentTransaction.commit(); // Insert this as info in the log Log.i(TAG, logMessage); } /** * Transitions state to some state of the class provided. * * @param stateClass * @return the existing or new state, useful when chaining. */ public <MS extends MapState> MS changeState(Class<? extends MapState> stateClass) { try { MS newState = (MS) stateClass.newInstance(); changeState(newState); return newState; } catch (Exception e) { throw new RuntimeException(e); } } /** * Get the current state of the MapActivity. * * @return */ public MapState getState() { return state; } public IBCMapView getMapView() { if (mapView == null) { throw new RuntimeException("The mapView has not yet been initialized."); } return mapView; } /** * Instantiate the left menu - this needs to be a method, so it can be overwritten. * * @return */ protected LeftMenu createLeftMenu() { return new LeftMenu(); } /** * Creates the material menu icon that animates when the drawer changes state. */ protected void createMenu() { // Creating the left menu fragment leftMenu = createLeftMenu(); // Find the drawer layout view using it's id, we'll attach the menu to that. drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); // Add the menu to the drawer layout getFragmentManager().beginTransaction().add(R.id.leftContainerDrawer, leftMenu).commit(); // We want the hamburger in the ActionBar materialMenu = new MaterialMenuIcon(this, Color.WHITE, MaterialMenuDrawable.Stroke.THIN); materialMenu.animateState(MaterialMenuDrawable.IconState.BURGER); // When the drawer opens or closes, we want the icon to animate between "burger" and "arrow" drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { @Override public void onDrawerOpened(View drawerView) { materialMenu.animateState(MaterialMenuDrawable.IconState.ARROW); } @Override public void onDrawerClosed(View drawerView) { materialMenu.animateState(MaterialMenuDrawable.IconState.BURGER); } @Override public void onDrawerStateChanged(int newState) { } @Override public void onDrawerSlide(View drawerView, float slideOffset) { } }); } /** * Checks if the app has sufficient permissions and updates the center of the map to the * current location upon receiving this. */ private void attemptToRegisterLocationListener() { boolean hasLocationPermissions = checkAndRequestLocationPermissions(); if (hasLocationPermissions && locationListener == null) { // We can register a location listener and start receiving location updates right away. locationListener = new LocationListener() { protected boolean hasUpdatedMap = false; @Override public void onLocationChanged(Location location) { // Let's update the map only once - we use the hasUpdatedMap for this // We cannot simply deregister the listener as this would stop updating the // user's location on the map. if (!hasUpdatedMap && state instanceof BrowsingState) { Log.d("MapActivity", "Location changed and we center the map"); mapView.setCenter(new LatLng(location)); hasUpdatedMap = true; } } }; Log.d("MapActivity", "Adding map's locationListener to the LocationService."); // We need a LocationListener to have the service be able to provide GPS coordinates. LOG.d("locationservice: " + IBikeApplication.getService()); IBikeApplication.getService().addLocationListener(locationListener); } } /** * Stop listening for locations by removing the location listener. */ private void deregisterLocationListener() { Log.d("MapActivity", "deregisterLocationListener called"); if (locationListener != null) { IBikeApplication.getService().removeLocationListener(locationListener); // Null this - as this is how we know if we've already added it to the location service. locationListener = null; } } /** * Checks if the app has permissions to the device's physical location and requests it if not. * The result of a permission request ends up calling the onRequestPermissionsResult method. */ private boolean checkAndRequestLocationPermissions() { final String permission = Manifest.permission.ACCESS_FINE_LOCATION; // Let's check if we have permissions to get file locations. int hasPermission = ContextCompat.checkSelfPermission(this.getApplicationContext(), permission); if (hasPermission != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] { permission }, PERMISSIONS_REQUEST_FINE_LOCATION); return false; } else { return true; } } /** * Result is back from a request for permissions. * If this request was for the device location, we attempt to register the location listener. */ @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PERMISSIONS_REQUEST_FINE_LOCATION: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d("MapActivity", "Got the permission to receive fine locations."); attemptToRegisterLocationListener(); } return; } default: { throw new RuntimeException("Request for permission was not handled"); } } } public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.ab_search) { this.mapView.clear(); Intent i = new Intent(MapActivity.this, SearchActivity.class); startActivityForResult(i, REQUEST_SEARCH_ADDRESS); overridePendingTransition(R.anim.slide_in_down, R.anim.fixed); } // Toggle the drawer when tapping the app icon. else if (id == android.R.id.home) { toggleDrawer(); } return super.onOptionsItemSelected(item); } protected void toggleDrawer() { if (drawerLayout == null) { throw new RuntimeException("toggleDrawer called too soon, drawerLayout was null"); } if (drawerLayout.isDrawerOpen(Gravity.LEFT)) { drawerLayout.closeDrawer(Gravity.LEFT); // Start the animation right away, instead of waiting for the drawer to settle. materialMenu.animateState(MaterialMenuDrawable.IconState.BURGER); } else { drawerLayout.openDrawer(Gravity.LEFT); // Start the animation right away, instead of waiting for the drawer to settle. materialMenu.animateState(MaterialMenuDrawable.IconState.ARROW); } } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); leftMenu.onResume(); } AlertDialog loginDlg; private void launchLoginDialog() { if (loginDlg == null) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(IBikeApplication.getString("login")); builder.setMessage(IBikeApplication.getString("error_not_logged_in")); builder.setPositiveButton(IBikeApplication.getString("login"), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent i = new Intent(MapActivity.this, LoginActivity.class); startActivity(i); } }); builder.setNegativeButton(IBikeApplication.getString("close"), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); loginDlg = builder.create(); } loginDlg.show(); } @Override public void onPause() { super.onPause(); deregisterLocationListener(); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); materialMenu.syncState(savedInstanceState); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); materialMenu.onSaveInstanceState(outState); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main_map_activity, menu); // Show the button to report problems, when in the navigating state. boolean navigating = state instanceof NavigatingState; return true; } public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(TAG, "onActivityResult, requestCode " + requestCode + " resultCode " + resultCode); if (requestCode == LeftMenu.LAUNCH_LOGIN) { Log.d(TAG, "Got back from the user login"); /* Log.d("JC", "Got back from LAUNCH_LOGIN"); if (!OverviewMapHandler.isWatchingAddress) { this.mapView.changeState(IBCMapView.MapViewState.DEFAULT); } leftMenu.populateMenu(); */ } else if (resultCode == ProfileActivity.RESULT_USER_DELETED) { Log.d(TAG, "Got back from deleting the user"); AlertDialog dialog; AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(IBikeApplication.getString("account_deleted")); builder.setPositiveButton(IBikeApplication.getString("close"), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); dialog = builder.create(); dialog.show(); } else if (requestCode == REQUEST_SEARCH_ADDRESS && resultCode == RESULT_OK) { Log.d(TAG, "Got back from address search, with an OK result"); // Change state right away DestinationPreviewState state = this.changeState(DestinationPreviewState.class); // What address was selected? final Bundle extras = data.getExtras(); Address address = (Address) extras.getSerializable("addressObject"); if (address != null) { if (address.getSource() == Address.Source.FAVORITE) { address.setHouseNumber(""); } state.setDestination(address); } else { throw new RuntimeException("Expected an address"); } } else if (requestCode == REQUEST_SEARCH_ADDRESS && resultCode == RESULT_CANCELED) { Log.d(TAG, "Got back from address search were the user canceled!"); // throw new UnsupportedOperationException("Canceling the search address has not been implemented."); } else if (requestCode == REQUEST_CHANGE_SOURCE_ADDRESS && resultCode == SearchAutocompleteActivity.RESULT_AUTOTOCMPLETE_SET) { Log.d(TAG, "Got back from setting the source"); if (state instanceof RouteSelectionState) { final Bundle extras = data.getExtras(); Address address = (Address) extras.getSerializable("addressObject"); if (address != null) { ((RouteSelectionState) state).setSource(address); } else { throw new RuntimeException("Expected an address"); } } } else if (requestCode == REQUEST_CHANGE_DESTINATION_ADDRESS && resultCode == SearchAutocompleteActivity.RESULT_AUTOTOCMPLETE_SET) { Log.d(TAG, "Got back from setting the destination"); if (state instanceof RouteSelectionState) { final Bundle extras = data.getExtras(); Address address = (Address) extras.getSerializable("addressObject"); if (address != null) { ((RouteSelectionState) state).setDestination(address); } else { throw new RuntimeException("Expected an address"); } } } else if (requestCode == LeftMenu.LAUNCH_FAVORITE) { Log.d(TAG, "Got back from the favorite screen."); // We got a favorite to navigate to if (resultCode == RESULT_OK) { // TODO: Re-implement launching of favorites // throw new UnsupportedOperationException("Launching favorites not yet implemented using MapStates"); FavoriteListItem fd = data.getExtras().getParcelable("ROUTE_TO"); Address address = fd.getAddress(); RouteSelectionState state = changeState(RouteSelectionState.class); state.setDestination(address); // mapView.showRoute(fd); //mapView.showAddressFromFavorite(a); // Close the LeftMenu drawerLayout.closeDrawer(Gravity.LEFT); } } else if (requestCode == LeftMenu.LAUNCH_TRACKING) { Log.d(TAG, "Got back from the tracking screen."); } else if (requestCode == LeftMenu.LAUNCH_ABOUT) { Log.d(TAG, "Got back from the about screen."); } } /** * Checks with all registered fragments if they're OK with letting back be pressed. * They should return false if they want to do something before letting the user continue back. */ public void onBackPressed() { if (drawerLayout.isDrawerOpen(Gravity.LEFT)) { drawerLayout.closeDrawer(Gravity.LEFT); } else if (state != null) { if (state.onBackPressed() == MapState.BackPressBehaviour.PROPAGATE) { super.onBackPressed(); } } else { super.onBackPressed(); } } public void readAloudClicked(View v) { if (getState() instanceof NavigatingState) { NavigatingState state = (NavigatingState) getState(); state.toggleReadAloud(); } } public void compassClicked(View v) { UserLocationOverlay.TrackingMode curMode = this.mapView.getUserLocationTrackingMode(); if (curMode == UserLocationOverlay.TrackingMode.NONE) { //this.mapView.setUserLocationEnabled(true); this.mapView.getUserLocationOverlay().enableFollowLocation(); this.mapView.setUserLocationTrackingMode(UserLocationOverlay.TrackingMode.FOLLOW); } else { //this.mapView.setUserLocationEnabled(false); this.mapView.getUserLocationOverlay().disableFollowLocation(); this.mapView.setUserLocationTrackingMode(UserLocationOverlay.TrackingMode.NONE); } } /** * Called when the user scrolls the map. Updates the compass. */ public void updateCompassIcon() { UserLocationOverlay.TrackingMode currentTrackingMode = mapView.getUserLocationTrackingMode(); // Without follow location enabled, we assume a NONE tracking mode. if (!mapView.getUserLocationOverlay().isFollowLocationEnabled()) { currentTrackingMode = UserLocationOverlay.TrackingMode.NONE; } if (previousTrackingMode != currentTrackingMode) { ImageButton userTrackingButton = (ImageButton) this.findViewById(R.id.userTrackingButton); if (currentTrackingMode == UserLocationOverlay.TrackingMode.NONE) { userTrackingButton.setImageDrawable(getResources().getDrawable(R.drawable.compass_not_tracking)); } else { userTrackingButton.setImageDrawable(getResources().getDrawable(R.drawable.compass_tracking)); } previousTrackingMode = currentTrackingMode; } } /** * Create the fragment used when the user selects a route, this is implemented as a function to * allow for implementations to override this behaviour. * @return */ public RouteSelectionFragment createRouteSelectionFragment() { IBikeApplication application = (IBikeApplication) getApplication(); if (application.breakRouteIsEnabled()) { return new BreakRouteSelectionFragment(); } else { return new RouteSelectionFragment(); } } }