/*
Montréal Just in Case
Copyright (C) 2011 Mudar Noufal <mn@mudar.ca>
Geographic locations of public safety services. A Montréal Open Data
project.
This file is part of Montréal Just in Case.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.mudar.mtlaucasou.ui.activity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.IntentSender;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v4.view.MenuItemCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.github.clans.fab.FloatingActionMenu;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.Marker;
import com.lsjwzh.widget.materialloadingprogressbar.CircleProgressBar;
import com.roughike.bottombar.BottomBar;
import com.roughike.bottombar.OnTabReselectListener;
import com.roughike.bottombar.OnTabSelectListener;
import java.util.List;
import ca.mudar.mtlaucasou.Const;
import ca.mudar.mtlaucasou.R;
import ca.mudar.mtlaucasou.data.RealmQueries;
import ca.mudar.mtlaucasou.data.UserPrefs;
import ca.mudar.mtlaucasou.model.MapType;
import ca.mudar.mtlaucasou.model.Placemark;
import ca.mudar.mtlaucasou.model.RealmPlacemark;
import ca.mudar.mtlaucasou.ui.adapter.PlacemarkInfoWindowAdapter;
import ca.mudar.mtlaucasou.ui.listener.LocationUpdatesManager;
import ca.mudar.mtlaucasou.ui.listener.MapLayersManager;
import ca.mudar.mtlaucasou.ui.listener.SearchResultsManager;
import ca.mudar.mtlaucasou.ui.view.PlacemarksSearchView;
import ca.mudar.mtlaucasou.util.EulaUtils;
import ca.mudar.mtlaucasou.util.LogUtils;
import ca.mudar.mtlaucasou.util.MapUtils;
import ca.mudar.mtlaucasou.util.MetricsUtils;
import ca.mudar.mtlaucasou.util.NavigUtils;
import ca.mudar.mtlaucasou.util.PermissionUtils;
import io.realm.Realm;
import io.realm.RealmChangeListener;
import io.realm.RealmResults;
import static ca.mudar.mtlaucasou.util.LogUtils.makeLogTag;
public class MainActivity extends BaseActivity implements
OnMapReadyCallback,
SearchResultsManager.MapUpdatesListener,
LocationUpdatesManager.LocationUpdatesCallbacks,
MapLayersManager.LayersFilterCallbacks {
private static final String TAG = makeLogTag("MainActivity");
private static final long BOTTOM_BAR_ANIM_DURATION = 200L; // 200ms
private static final long PROGRESS_BAR_ANIM_DURATION = 750L; // 750ms
private GoogleMap vMap;
private View mSnackbarParent;
private CircleProgressBar vProgressBar;
private FloatingActionButton mMyLocationFAB;
private MapLayersManager mLayersManager;
private BottomBar mBottomBar;
@MapType
private String mMapType;
private Realm mRealm;
private RealmChangeListener mRealmListener;
private Handler mHandler = new Handler(); // Waits for the BottomBar anim
private LocationUpdatesManager mLocationManger;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
// Avoid showing EULA twice on orientation change
EulaUtils.showEulaIfNecessary(this);
}
setTitle(R.string.title_activity_main);
setContentView(R.layout.activity_main);
vProgressBar = (CircleProgressBar) findViewById(R.id.progressBar);
mSnackbarParent = findViewById(R.id.map_wrapper);
mRealm = Realm.getDefaultInstance();
mLocationManger = new LocationUpdatesManager(MainActivity.this, this);
setupMap();
setupFAB();
final @MapType String lastMapType = UserPrefs.getInstance(getApplicationContext()).getLastMapType();
setupBottomBar(lastMapType);
setMapType(lastMapType, 0);
}
protected void onStart() {
super.onStart();
mLocationManger.onStart();
toggleMyLocationButton();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
setupSearchView(menu);
return true;
}
@Override
protected void onStop() {
super.onStop();
try {
mLocationManger.onStop();
UserPrefs.getInstance(getApplicationContext()).setLastMapType(mMapType);
} catch (Exception e) {
LogUtils.REMOTE_LOG(e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
// Remove all change listeners to avoid leaks
mRealm.removeAllChangeListeners();
mRealm.close();
} catch (Exception e) {
LogUtils.REMOTE_LOG(e);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (Const.RequestCodes.LOCATION_SETTINGS_CHANGE == requestCode) {
onLocationSettingsActivityResult(resultCode, data);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == Const.RequestCodes.LOCATION_PERMISSION) {
if (PermissionUtils.checkLocationPermission(this)) {
UserPrefs.getInstance(getApplicationContext()).setPermissionDeniedForEver(true);
mMyLocationFAB.show();
MapUtils.enableMyLocation(this, vMap);
mLocationManger.onLocationPermissionGranted();
} else {
PermissionUtils.showLocationRationaleOrSurrender(this, mSnackbarParent);
mMyLocationFAB.hide();
mMyLocationFAB.setVisibility(View.GONE);
}
}
}
private void setupSearchView(final Menu menu) {
// Get the toolbar menu SearchView
final MenuItem searchMenuItem = menu.findItem(R.id.action_search);
final PlacemarksSearchView searchView =
(PlacemarksSearchView) MenuItemCompat.getActionView(searchMenuItem);
try {
searchView.setSearchMenuItem(searchMenuItem);
searchView.setListener(new SearchResultsManager(MainActivity.this, this));
} catch (Exception e) {
LogUtils.REMOTE_LOG(e);
searchMenuItem.setVisible(false);
}
}
/**
* Show the bottom bar navigation items
*/
private void setupBottomBar(final @MapType String type) {
mBottomBar = (BottomBar) findViewById(R.id.bottom_bar);
assert mBottomBar != null;
mBottomBar.setDefaultTab(NavigUtils.getTabIdByMapType(type));
mBottomBar.setOnTabSelectListener(new OnTabSelectListener() {
@Override
public void onTabSelected(@IdRes final int tabId) {
final @MapType String mapType = NavigUtils.getMapTypeByTabId(tabId);
showcaseMapLayers(mapType);
setMapType(mapType, BOTTOM_BAR_ANIM_DURATION);
}
});
mBottomBar.setOnTabReselectListener(new OnTabReselectListener() {
@Override
public void onTabReSelected(@IdRes int tabId) {
if (isMapReady()) {
vMap.animateCamera(CameraUpdateFactory.zoomTo(Const.ZOOM_OUT));
}
mLayersManager.toggleFilterMenu();
}
});
}
private void setupFAB() {
mMyLocationFAB = (FloatingActionButton) findViewById(R.id.fab);
mMyLocationFAB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (PermissionUtils.checkLocationPermission(MainActivity.this)) {
MapUtils.moveCameraToLocation(vMap, mLocationManger.getUserLocation(), true);
} else {
PermissionUtils.requestLocationPermission(MainActivity.this);
}
}
});
mLayersManager = new MapLayersManager(this, (FloatingActionMenu) findViewById(R.id.fab_menu), this);
}
/**
* Obtain the SupportMapFragment and get notified when the map is ready to be used.
*/
private void setupMap() {
try {
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentByTag(Const.FragmentTags.MAP);
if (mapFragment == null) {
mapFragment = new SupportMapFragment();
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.content_frame, mapFragment, Const.FragmentTags.MAP)
.commit();
mapFragment.getMapAsync(this);
} catch (Exception e) {
LogUtils.REMOTE_LOG(e);
isMapReady();
}
}
/**
* Manipulate the map once available.
*
* @param googleMap The GoogleMap
*/
@Override
public void onMapReady(GoogleMap googleMap) {
vMap = googleMap;
vMap.getUiSettings().setMyLocationButtonEnabled(false);
// Setup the InfoWindow
@SuppressLint("InflateParams")
final View vMarkerInfoWindow = getLayoutInflater()
.inflate(R.layout.custom_info_window, null, false);
vMap.setInfoWindowAdapter(new PlacemarkInfoWindowAdapter(vMarkerInfoWindow, mLocationManger));
// Handle location
MapUtils.setupMapLocation(vMap);
MapUtils.enableMyLocation(this, vMap);
mLocationManger.setGoogleMap(vMap);
loadMapData(mMapType);
mLayersManager.setMap(vMap);
}
/**
* Verify if GoogleMap has loaded correctly.
* Utility method to handle a Google Play Services error
* Ref: https://code.google.com/p/gmaps-api-issues/issues/detail?id=5100
*
* @return true if onMapReady() has been successfully called
*/
private boolean isMapReady() {
if (vMap == null) {
Snackbar.make(mSnackbarParent, R.string.snackbar_google_maps_error, Snackbar.LENGTH_LONG)
.setAction(R.string.btn_retry, new View.OnClickListener() {
@Override
public void onClick(View view) {
setupMap();
}
})
.show();
return false;
}
return true;
}
private void setMapType(final @MapType String type, long delay) {
mMapType = type;
final boolean hasFilterMenu = mLayersManager.toggleFilterMenu(type);
if (isMapReady()) {
toggleProgressBar(true);
// Remove previous markers
MapUtils.clearMap(vMap);
mHandler.removeCallbacksAndMessages(null);
// Wait for the BottomBar animation to end before loading data
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// Map was already cleared, to show user something is happening!
loadMapData(type);
}
}, delay);
}
MetricsUtils.logMapView(type);
}
/**
* Load the cached data, or request download
*
* @param type
*/
private void loadMapData(@MapType final String type) {
if (!isMapReady()) {
return;
}
if (mRealmListener != null) {
// Remove previously added RealmChangeListener
mRealm.removeChangeListener(mRealmListener);
mRealmListener = null;
}
// First, query the Realm db for the current mapType
final RealmResults<RealmPlacemark> realmPlacemarks = RealmQueries
.queryPlacemarksByMapType(mRealm,
type,
UserPrefs.getInstance(getApplicationContext()).getEnabledLayers()
).findAll();
if (realmPlacemarks.size() > 0) {
// Has cached data
final List<Marker> markers = MapUtils.addPlacemarksToMap(vMap, realmPlacemarks);
new Handler().postDelayed(new Runnable() {
/**
* Delay hiding the progressbar for 750ms, avoids blink-effect on fast operations.
* And allows findAndShowNearestMarker() to wait for the real center in case
* of camera animation.
*/
@Override
public void run() {
toggleProgressBar(false);
MapUtils.findAndShowNearestMarker(vMap, markers, mSnackbarParent);
}
}, PROGRESS_BAR_ANIM_DURATION);
} else {
// Add a change listener for empty data only, to avoid showing empty maps.
// Remote updates will be shown on tab changes. This is not an issue for our app
// because of the low frequency/value of remote data updates.
mRealmListener = new RealmChangeListener() {
@Override
public void onChange(Object element) {
loadMapData(type);
}
};
mRealm.addChangeListener(mRealmListener);
}
}
@Override
public void moveCameraToPlacemark(Placemark placemark) {
GoogleMap.OnCameraIdleListener cameraIdleListener;
// Enable this placemark's layer if necessary
final UserPrefs prefs = UserPrefs.getInstance(getApplicationContext());
final boolean updateLayers = !prefs.isLayerEnabled(placemark.getLayerType());
if (updateLayers) {
prefs.setLayerEnabledForced(placemark.getMapType(), placemark.getLayerType());
mLayersManager.setupEnabledLayers(prefs);
}
if (mMapType != null && !mMapType.equals(placemark.getMapType())) {
final int tabId = NavigUtils.getTabIdByMapType(placemark.getMapType());
cameraIdleListener = new GoogleMap.OnCameraIdleListener() {
/**
* We need to switch mapType after the camera animation. Selecting the tab
* triggers a call to setMapType() which clears map and loads data
*/
@Override
public void onCameraIdle() {
mBottomBar.selectTabWithId(tabId);
}
};
} else if (updateLayers) {
cameraIdleListener = new GoogleMap.OnCameraIdleListener() {
/**
* After the camera animation, we need to
*/
@Override
public void onCameraIdle() {
onFiltersApply();
}
};
} else {
// Selected placemark is of current type: ignore
cameraIdleListener = null;
}
MapUtils.moveCameraToPlacemark(vMap, placemark, true, cameraIdleListener);
}
@Override
public void moveCameraToLocation(Location location) {
MapUtils.moveCameraToLocation(vMap, location, true);
}
private void toggleProgressBar(boolean visible) {
if (visible) {
vProgressBar.setColorSchemeColors(MapUtils.getMapTypeColor(this, mMapType));
vProgressBar.setVisibility(View.VISIBLE);
} else {
vProgressBar.setVisibility(View.GONE);
}
}
/**
* Verify if user has changed his mind about denying permission forever.
* This method uses setVisibility() instead of show/hide methods.
*/
private void toggleMyLocationButton() {
if (PermissionUtils.checkPermissionWasDeniedForEver(getApplicationContext())) {
mMyLocationFAB.hide();
} else {
mMyLocationFAB.show();
}
}
/**
* Implements LocationUpdatesManager.LocationUpdatesCallbacks
*
* @param status
* @throws IntentSender.SendIntentException
*/
@Override
public void requestLocationSettingsChange(Status status)
throws IntentSender.SendIntentException {
status.startResolutionForResult(
MainActivity.this,
Const.RequestCodes.LOCATION_SETTINGS_CHANGE);
}
/**
* Implements LocationUpdatesManager.LocationUpdatesCallbacks
*/
@Override
public void onLocationSettingsActivityResult(int resultCode, Intent data) {
mLocationManger.onLocationSettingsResult(resultCode, data);
}
/**
* MapLayersManager.LayersFilterCallbacks
*/
@Override
public void onFiltersChange() {
MapUtils.clearMap(vMap);
}
/**
* MapLayersManager.LayersFilterCallbacks
*/
@Override
public void onFiltersApply() {
toggleProgressBar(true);
loadMapData(mMapType);
}
}