/* 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 org.mozilla.mozstumbler.client.mapview; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Paint; import android.location.Location; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Build; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.mozstumbler.BuildConfig; import org.mozilla.mozstumbler.R; import org.mozilla.mozstumbler.client.ClientPrefs; import org.mozilla.mozstumbler.client.MainApp; import org.mozilla.mozstumbler.client.ObservedLocationsReceiver; import org.mozilla.mozstumbler.client.mapview.maplocation.UserPositionUpdateManager; import org.mozilla.mozstumbler.client.mapview.tiles.AbstractMapOverlay; import org.mozilla.mozstumbler.client.mapview.tiles.CoverageOverlay; import org.mozilla.mozstumbler.client.mapview.tiles.LowResMapOverlay; import org.mozilla.mozstumbler.client.navdrawer.MetricsView; import org.mozilla.mozstumbler.service.AppGlobals; import org.mozilla.mozstumbler.service.core.http.IHttpUtil; import org.mozilla.mozstumbler.service.core.logging.ClientLog; import org.mozilla.mozstumbler.service.stumblerthread.scanners.GPSScanner; import org.mozilla.mozstumbler.svclocator.ServiceLocator; import org.mozilla.mozstumbler.svclocator.services.log.ILogger; import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil; import org.mozilla.osmdroid.ResourceProxy; import org.mozilla.osmdroid.api.IGeoPoint; import org.mozilla.osmdroid.events.DelayedMapListener; import org.mozilla.osmdroid.events.MapListener; import org.mozilla.osmdroid.events.ScrollEvent; import org.mozilla.osmdroid.events.ZoomEvent; import org.mozilla.osmdroid.tileprovider.BitmapPool; import org.mozilla.osmdroid.tileprovider.MapTile; import org.mozilla.osmdroid.tileprovider.tilesource.ITileSource; import org.mozilla.osmdroid.tileprovider.tilesource.OnlineTileSourceBase; import org.mozilla.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.mozilla.osmdroid.tileprovider.tilesource.XYTileSource; import org.mozilla.osmdroid.util.GeoPoint; import org.mozilla.osmdroid.views.MapView; import org.mozilla.osmdroid.views.overlay.Overlay; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicBoolean; public class MapFragment extends android.support.v4.app.Fragment implements MetricsView.IMapLayerToggleListener { private static final ILogger Log = (ILogger) ServiceLocator.getInstance().getService(ILogger.class); private static final String LOG_TAG = LoggerUtil.makeLogTag(MapFragment.class); private static final String COVERAGE_REDIRECT_URL = "https://location.services.mozilla.com/map.json"; private static final String ZOOM_KEY = "zoom"; public static final int LOWEST_UNLIMITED_ZOOM = 3; private static final int DEFAULT_ZOOM = 13; private static final int DEFAULT_ZOOM_AFTER_FIX = 16; private static final String LAT_KEY = "latitude"; private static final String LON_KEY = "longitude"; private static final int HIGH_ZOOM_THRESHOLD = 14; private static String sCoverageUrl; // Only used by CoverageSetup private final Timer mGetUrl = new Timer(); private MapView mMap; private AccuracyCircleOverlay mAccuracyOverlay; private static boolean sHadFirstLocationFix = false; private static boolean sUserPanning = true; private ObservationPointsOverlay mObservationPointsOverlay; private UserPositionUpdateManager mMapLocationListener; private LowResMapOverlay mLowResMapOverlayHighZoom; private LowResMapOverlay mLowResMapOverlayLowZoom; private Overlay mCoverageTilesOverlayLowZoom; private Overlay mCoverageTilesOverlayHighZoom; private Overlay mCoverageTilesOverlayOriginalZoom; // used in unlimited zoom mode for levels < LOW_ZOOM_LEVEL private ITileSource mHighResMapSource; private View mRootView; private TextView mTextViewMapResolutionInfo; private HighLowBandwidthReceiver mHighLowBandwidthChecker; private CoverageSetup mCoverageSetup = new CoverageSetup(); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); mRootView = inflater.inflate(R.layout.activity_map, container, false); doOnCreateView(savedInstanceState); return mRootView; } /* This method has been extracted from onCreateView so that it's easy to stub it out when the class is under test. */ void doOnCreateView(Bundle savedInstanceState) { MainApp app = getApplication(); if (app == null) { return; } AbstractMapOverlay.setDisplayBasedMinimumZoomLevel(app); showMapNotAvailableMessage(NoMapAvailableMessage.eHideNoMapMessage); hideLowResMapMessage(); initializeMapControls(); initializeLastLocation(savedInstanceState); initializeMapOverlays(); initializeVisibleCounts(); initializeListeners(); showPausedDueToNoMotionMessage(app.isIsScanningPausedDueToNoMotion()); showCopyright(); } private void hideLowResMapMessage() { mTextViewMapResolutionInfo = (TextView) mRootView.findViewById(R.id.resolution_info_map_message); mTextViewMapResolutionInfo.setVisibility(View.GONE); } private void initializeMapControls() { mMap = (MapView) mRootView.findViewById(R.id.map); mMap.setBuiltInZoomControls(true); mMap.setMultiTouchControls(true); } private void initializeCenterMeListener() { final ImageButton centerMe = (ImageButton) mRootView.findViewById(R.id.my_location_button); updateCenterButtonIcon(); centerMe.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mAccuracyOverlay == null || mAccuracyOverlay.getLocation() == null) return; mMap.getController().animateTo((mAccuracyOverlay.getLocation())); sUserPanning = false; updateCenterButtonIcon(); } }); } private void initializeListeners() { initializeCenterMeListener(); mMap.getOverlays().add(new SwipeListeningOverlay(getActivity().getApplicationContext(), new SwipeListeningOverlay.OnSwipeListener() { @Override public void onSwipe() { sUserPanning = true; updateCenterButtonIcon(); } })); mMap.setMapListener(new DelayedMapListener(new MapListener() { @Override public boolean onZoom(final ZoomEvent e) { // This is key to no-wifi (low-res) tile functions, that // when the zoom level changes, we check to see if the // low-zoom or high-zoom should be shown int z = e.getZoomLevel(); updateOverlayBaseLayer(z); updateOverlayCoverageLayer(z); mObservationPointsOverlay.zoomChanged(mMap); return true; } @Override public boolean onScroll(final ScrollEvent e) { return true; } }, 0)); ObservedLocationsReceiver observer = ObservedLocationsReceiver.getInstance(); observer.setMapActivity(this); } private void initializeMapOverlays() { mAccuracyOverlay = new AccuracyCircleOverlay(mRootView.getContext(), getResources().getColor(R.color.gps_track)); mMap.getOverlays().add(mAccuracyOverlay); mObservationPointsOverlay = new ObservationPointsOverlay(mRootView.getContext()); mMap.getOverlays().add(mObservationPointsOverlay); } private void initializeLastLocation(Bundle savedInstanceState) { int zoomLevel; GeoPoint lastLoc = null; if (savedInstanceState != null) { zoomLevel = savedInstanceState.getInt(ZOOM_KEY, DEFAULT_ZOOM); if (savedInstanceState.containsKey(LAT_KEY) && savedInstanceState.containsKey(LON_KEY)) { final double latitude = savedInstanceState.getDouble(LAT_KEY); final double longitude = savedInstanceState.getDouble(LON_KEY); lastLoc = new GeoPoint(latitude, longitude); } } else { lastLoc = ClientPrefs.getInstance(mRootView.getContext()).getLastMapCenter(); zoomLevel = DEFAULT_ZOOM_AFTER_FIX; if (new GeoPoint(0, 0).equals(lastLoc)) { lastLoc = null; } } if (lastLoc != null) { final GeoPoint loc = lastLoc; final int zoom = zoomLevel; setCenterAndZoom(loc, zoom); mMap.postDelayed(new Runnable() { @Override public void run() { // https://github.com/osmdroid/osmdroid/issues/22 // These need a fully constructed map, which on first load seems to take a while. // Post with no delay does not work for me, adding an arbitrary // delay of 300 ms should be plenty. ClientLog.d(LOG_TAG, "postDelayed ZOOM " + zoom); setCenterAndZoom(loc, zoom); } }, 300); } } private void initializeVisibleCounts() { Configuration c = getResources().getConfiguration(); if (c.fontScale > 1) { ClientLog.d(LOG_TAG, "Large text is enabled: " + c.fontScale); mRootView.findViewById(R.id.text_satellites_sep).setVisibility(View.GONE); mRootView.findViewById(R.id.text_satellites_avail).setVisibility(View.GONE); } else { initTextView(R.id.text_satellites_avail, "00"); } initTextView(R.id.text_cells_visible, "000"); initTextView(R.id.text_wifis_visible, "000"); initTextView(R.id.text_observation_count, "00000"); } private void setCenterAndZoom(GeoPoint loc, int zoom) { mMap.getController().setZoom(zoom); mMap.getController().setCenter(loc); } public MainApp getApplication() { FragmentActivity activity = getActivity(); if (activity == null) { return null; } return (MainApp) activity.getApplication(); } private void initCoverageTiles(String coverageUrl) { ClientLog.i(LOG_TAG, "initCoverageTiles: " + coverageUrl); mCoverageTilesOverlayLowZoom = new CoverageOverlay(AbstractMapOverlay.TileResType.LOWER_ZOOM, mRootView.getContext(), coverageUrl, mMap); mCoverageTilesOverlayHighZoom = new CoverageOverlay(AbstractMapOverlay.TileResType.HIGHER_ZOOM, mRootView.getContext(), coverageUrl, mMap); mCoverageTilesOverlayOriginalZoom = new CoverageOverlay(AbstractMapOverlay.TileResType.ORIGINAL_ZOOM, mRootView.getContext(), coverageUrl, mMap); } // // This determines which level of detail of tile layer is shown. // private boolean isHighZoom(int zoomLevel) { return zoomLevel > HIGH_ZOOM_THRESHOLD; } // If the map is not in low res mode, return. // Otherwise, set the low res overlay based on the current zoom level, and set it to a // lower resolution than the zoom level of the map (trick it into showing lower resolution // tiles than the map normally would at a given zoom level) private void updateOverlayBaseLayer(int zoomLevel) { if (mLowResMapOverlayHighZoom == null || mLowResMapOverlayLowZoom == null) { return; } final List<Overlay> overlays = mMap.getOverlays(); final Overlay overlayRemoved = (!isHighZoom(zoomLevel)) ? mLowResMapOverlayHighZoom : mLowResMapOverlayLowZoom; final Overlay overlayAdded = (isHighZoom(zoomLevel)) ? mLowResMapOverlayHighZoom : mLowResMapOverlayLowZoom; overlays.remove(overlayRemoved); if (!overlays.contains(overlayAdded)) { overlays.add(0, overlayAdded); mMap.invalidate(); } } // At regular zoom levels, the MLS coverage follows the same logic as the lower resolution map // overlay, in that when at low zoom level, show even lower resolution tiles. // At lower zoom levels (only available in unlimited zoom mode) show exact zoom level private void updateOverlayCoverageLayer(int zoomLevel) { if (mCoverageTilesOverlayLowZoom == null || mCoverageTilesOverlayHighZoom == null || mCoverageTilesOverlayOriginalZoom == null) { return; } final List<Overlay> overlays = mMap.getOverlays(); if (zoomLevel < CoverageOverlay.LOW_ZOOM_LEVEL) { if (!overlays.contains(mCoverageTilesOverlayOriginalZoom)) { overlays.remove(mCoverageTilesOverlayLowZoom); overlays.remove(mCoverageTilesOverlayHighZoom); overlays.add(0, mCoverageTilesOverlayOriginalZoom); mMap.invalidate(); } return; } final Overlay overlayRemoved = (!isHighZoom(zoomLevel)) ? mCoverageTilesOverlayHighZoom : mCoverageTilesOverlayLowZoom; final Overlay overlayAdded = (isHighZoom(zoomLevel)) ? mCoverageTilesOverlayHighZoom : mCoverageTilesOverlayLowZoom; overlays.remove(overlayRemoved); overlays.remove(mCoverageTilesOverlayOriginalZoom); if (!overlays.contains(overlayAdded)) { boolean hasMapOverlay = overlays.contains(mLowResMapOverlayHighZoom) || overlays.contains(mLowResMapOverlayLowZoom); int idx = hasMapOverlay ? 1 : 0; overlays.add(idx, overlayAdded); mMap.invalidate(); } } // Unfortunately, just showing low/high detail isn't enough data reduction. // To handle the case where the user zooms out to show a large area when in low bandwidth mode, // we need an additional "LowZoom" overlay. So in low bandwidth mode, you will see // that based on the current zoom level of the map, we show "HighZoom" or "LowZoom" overlays. void setHighBandwidthMap(boolean hasNetwork, boolean isHighBandwidth) { final ClientPrefs prefs = ClientPrefs.getInstance(mRootView.getContext()); if (prefs == null || getActivity() == null) { return; } final ClientPrefs.MapTileResolutionOptions tileType = prefs.getMapTileResolutionType(); if (tileType != ClientPrefs.MapTileResolutionOptions.Default) { hasNetwork = true; // always update the map type if (tileType == ClientPrefs.MapTileResolutionOptions.NoMap) { updateMapResolutionTextView(tileType, isHighBandwidth); mMap.setTileSource(new BlankTileSource()); removeLayer(mLowResMapOverlayLowZoom); removeLayer(mLowResMapOverlayHighZoom); removeLayer(mCoverageTilesOverlayLowZoom); removeLayer(mCoverageTilesOverlayHighZoom); mLowResMapOverlayHighZoom = mLowResMapOverlayLowZoom = null; mCoverageTilesOverlayHighZoom = mCoverageTilesOverlayLowZoom = null; return; } else if (tileType == ClientPrefs.MapTileResolutionOptions.HighRes) { isHighBandwidth = true; } else if (tileType == ClientPrefs.MapTileResolutionOptions.LowRes) { isHighBandwidth = false; } } else if (!hasNetwork) { isHighBandwidth = true; // use this as initial default } final boolean isMLSTileStore = (BuildConfig.TILE_SERVER_URL != null); final boolean hasHighResMap = mLowResMapOverlayHighZoom == null && mMap.getTileProvider().getTileSource() == mHighResMapSource; final boolean hasLowResMap = mLowResMapOverlayHighZoom != null; if (!hasNetwork && (hasHighResMap || hasLowResMap)) { updateMapResolutionTextView(tileType, hasHighResMap); return; } if (isHighBandwidth && !hasHighResMap) { removeLayer(mLowResMapOverlayHighZoom); removeLayer(mLowResMapOverlayLowZoom); mLowResMapOverlayLowZoom = null; mLowResMapOverlayHighZoom = null; // We've destroyed 2 layers for lowResMapOverlay // Force GC to cleanup underlying LRU caches in overlay System.gc(); if (!isMLSTileStore) { mHighResMapSource = TileSourceFactory.MAPQUESTOSM; } else { mHighResMapSource = new XYTileSource(AbstractMapOverlay.MLS_MAP_TILE_BASE_NAME, null, 1, AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP, AbstractMapOverlay.TILE_PIXEL_SIZE, AbstractMapOverlay.FILE_TYPE_SUFFIX_PNG, new String[]{BuildConfig.TILE_SERVER_URL}); } System.gc(); mMap.setTileSource(mHighResMapSource); } else if (!isHighBandwidth && !hasLowResMap) { // Unhooking the highres map means we should nullify it and force GC // to cleanup underlying LRU cache in MapSource mHighResMapSource = null; System.gc(); mMap.setTileSource(new BlankTileSource()); mLowResMapOverlayLowZoom = new LowResMapOverlay(AbstractMapOverlay.TileResType.LOWER_ZOOM, this.getActivity(), isMLSTileStore, mMap); mLowResMapOverlayHighZoom = new LowResMapOverlay(AbstractMapOverlay.TileResType.HIGHER_ZOOM, this.getActivity(), isMLSTileStore, mMap); updateOverlayBaseLayer(mMap.getZoomLevel()); } updateMapResolutionTextView(tileType, isHighBandwidth); int minZoom = AbstractMapOverlay.getDisplaySizeBasedMinZoomLevel(); if (prefs.isMapZoomUnlimited()) { minZoom = isHighBandwidth ? LOWEST_UNLIMITED_ZOOM : LowResMapOverlay.LOW_ZOOM_LEVEL; } mMap.setMinZoomLevel(minZoom); } private void updateMapResolutionTextView(ClientPrefs.MapTileResolutionOptions tileType, boolean isHighBandwidth) { String text = ""; int visibility = View.VISIBLE; if (tileType == ClientPrefs.MapTileResolutionOptions.Default) { if (isHighBandwidth) { visibility = View.GONE; } else { text = getActivity().getString(R.string.low_resolution_map); } } else { if (tileType == ClientPrefs.MapTileResolutionOptions.NoMap) { text = getActivity().getString(R.string.map_turned_off); } else { final String[] labels = getActivity().getResources().getStringArray(R.array.map_tile_resolution_options); text = labels[tileType.ordinal()]; } } mTextViewMapResolutionInfo.setText(text); mTextViewMapResolutionInfo.setVisibility(visibility); } public void mapNetworkConnectionChanged() { FragmentActivity activity = getActivity(); if (activity == null) { // This is only null because of roboelectric return; } if (activity.getFilesDir() == null) { // Not the ideal spot for this check perhaps, but there is no point in checking // the network when storage is not available. showMapNotAvailableMessage(NoMapAvailableMessage.eNoMapDueToNoAccessibleStorage); return; } getUrlAndInit(); // Note that under test, roboelectric will return null from getActivity(), but // getActivity().getSystemService(...) will properly return the ShadowConnectivityManager final ConnectivityManager cm = (ConnectivityManager) getActivity().getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo info = cm.getActiveNetworkInfo(); final boolean hasNetwork = (info != null) && info.isConnected(); final boolean hasWifi = (info != null) && (info.getType() == ConnectivityManager.TYPE_WIFI); NoMapAvailableMessage message = hasNetwork ? NoMapAvailableMessage.eHideNoMapMessage : NoMapAvailableMessage.eNoMapDueToNoInternet; showMapNotAvailableMessage(message); setHighBandwidthMap(hasNetwork, hasWifi); } void getUrlAndInit() { mCoverageSetup.getUrlAndInit(); } public void setZoomButtonsVisible(boolean visible) { mMap.setZoomButtonsVisible(visible); } @SuppressLint("NewApi") public void dimToolbar() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { return; } View v = mRootView.findViewById(R.id.status_toolbar_layout); final MainApp app = getApplication(); float alpha = 0.5f; if (app != null && app.isScanning()) { alpha = 1.0f; } v.setAlpha(alpha); } public void toggleScanning(MenuItem menuItem) { MainApp app = getApplication(); if (app == null) { return; } boolean isScanning = app.isScanningOrPaused(); if (isScanning) { app.stopScanning(); } else { app.startScanning(); } dimToolbar(); } private void showCopyright() { TextView copyrightArea = (TextView) mRootView.findViewById(R.id.copyright_area); if (BuildConfig.TILE_SERVER_URL == null) { copyrightArea.setText(getActivity().getString(R.string.map_copyright_fdroid)); } else { copyrightArea.setText(getActivity().getString(R.string.map_copyright_moz)); } } private void updateCenterButtonIcon() { ImageButton centerMe = (ImageButton) mRootView.findViewById(R.id.my_location_button); centerMe.setVisibility(sHadFirstLocationFix ? View.VISIBLE : View.INVISIBLE); if (sUserPanning) { centerMe.setBackgroundResource(R.drawable.ic_mylocation_no_dot_android_assets); } else { centerMe.setBackgroundResource(R.drawable.ic_mylocation_android_assets); } } public void setUserPositionAt(Location location) { mAccuracyOverlay.setLocation(location); if (!sHadFirstLocationFix) { setCenterAndZoom(new GeoPoint(location), DEFAULT_ZOOM_AFTER_FIX); sHadFirstLocationFix = true; sUserPanning = false; updateCenterButtonIcon(); } else if (!sUserPanning) { mMap.getController().animateTo((mAccuracyOverlay.getLocation())); } else { mMap.postInvalidate(); } } public void updateGPSInfo(int satellites, int fixes) { formatTextView(R.id.text_satellites_avail, "%d", satellites); formatTextView(R.id.text_satellites_used, "%d", fixes); // @TODO this is still not accurate int icon = fixes >= GPSScanner.MIN_SAT_USED_IN_FIX ? R.drawable.ic_gps_receiving_flaticondotcom : R.drawable.ic_gps_no_signal_flaticondotcom; ((ImageView) mRootView.findViewById(R.id.fix_indicator)).setImageResource(icon); } @Override public void onResume() { super.onResume(); ClientLog.d(LOG_TAG, "onResume"); doOnResume(); } /* This method has been extracted from onResume so that we can just disable the behavior when the class is under test */ void doOnResume() { ClientPrefs prefs = ClientPrefs.getInstance(getActivity().getApplicationContext()); mMapLocationListener = new UserPositionUpdateManager(this, prefs.isScanningPassive()); ObservedLocationsReceiver observer = ObservedLocationsReceiver.getInstance(); observer.setMapActivity(this); dimToolbar(); mapNetworkConnectionChanged(); mHighLowBandwidthChecker = new HighLowBandwidthReceiver(this); setShowMLS(prefs.showMLSQueryResults()); mObservationPointsOverlay.zoomChanged(mMap); mMap.postInvalidate(); } private void saveStateToPrefs() { IGeoPoint center = mMap.getMapCenter(); ClientPrefs.getInstance(mRootView.getContext()).setLastMapCenter(center); } @Override public void onSaveInstanceState(Bundle bundle) { super.onSaveInstanceState(bundle); bundle.putInt(ZOOM_KEY, mMap.getZoomLevel()); IGeoPoint center = mMap.getMapCenter(); bundle.putDouble(LON_KEY, center.getLongitude()); bundle.putDouble(LAT_KEY, center.getLatitude()); saveStateToPrefs(); } @Override public void onPause() { super.onPause(); ClientLog.d(LOG_TAG, "onPause"); saveStateToPrefs(); if (mMapLocationListener != null) { mMapLocationListener.removeListener(); mMapLocationListener = null; } ObservedLocationsReceiver observer = ObservedLocationsReceiver.getInstance(); observer.removeMapActivity(); MainApp app = getApplication(); if (app == null) { return; } mHighLowBandwidthChecker.unregister(app); } private void removeLayer(Overlay layer) { if (layer == null) { return; } mMap.getOverlays().remove(layer); layer.onDetach(mMap); } @Override public void onDestroy() { super.onDestroy(); ClientLog.d(LOG_TAG, "onDestroy"); removeLayer(mCoverageTilesOverlayOriginalZoom); removeLayer(mLowResMapOverlayHighZoom); removeLayer(mLowResMapOverlayLowZoom); removeLayer(mCoverageTilesOverlayHighZoom); removeLayer(mCoverageTilesOverlayLowZoom); mMap.getTileProvider().clearTileCache(); BitmapPool.getInstance().clearBitmapPool(); } public void formatTextView(int textViewId, int stringId, Object... args) { String str = getResources().getString(stringId); formatTextView(textViewId, str, args); } public void formatTextView(int textViewId, String str, Object... args) { TextView textView = (TextView) mRootView.findViewById(textViewId); str = String.format(str, args); textView.setText(str); } private void initTextView(int textViewId, String bound) { TextView textView = (TextView) mRootView.findViewById(textViewId); Paint textPaint = textView.getPaint(); int width = (int) Math.ceil(textPaint.measureText(bound)); textView.setWidth(width); textView.getLayoutParams().width = width; textView.setText("0"); } public void newMLSPoint(ObservationPoint point) { mObservationPointsOverlay.update(point, mMap, true); } public void newObservationPoint(ObservationPoint point) { mObservationPointsOverlay.update(point, mMap, false); } @Override public void setShowMLS(boolean isOn) { mObservationPointsOverlay.mOnMapShowMLS = isOn; mMap.invalidate(); } public void showMapNotAvailableMessage(NoMapAvailableMessage noMapAvailableMessage) { TextView noMapMessage = (TextView) mRootView.findViewById(R.id.message_area); if (noMapAvailableMessage == NoMapAvailableMessage.eHideNoMapMessage) { noMapMessage.setVisibility(View.INVISIBLE); } else { noMapMessage.setVisibility(View.VISIBLE); int resId = (noMapAvailableMessage == NoMapAvailableMessage.eNoMapDueToNoInternet) ? R.string.map_offline_mode : R.string.map_unavailable; noMapMessage.setText(resId); } } public void showPausedDueToNoMotionMessage(boolean show) { mRootView.findViewById(R.id.scanning_paused_message).setVisibility(show ? View.VISIBLE : View.INVISIBLE); updateGPSInfo(0, 0); dimToolbar(); } public void stop() { mRootView.findViewById(R.id.scanning_paused_message).setVisibility(View.INVISIBLE); updateGPSInfo(0, 0); dimToolbar(); } public static enum NoMapAvailableMessage {eHideNoMapMessage, eNoMapDueToNoAccessibleStorage, eNoMapDueToNoInternet} // An overlay for the sole purpose of reporting a user swiping on the map private static class SwipeListeningOverlay extends Overlay { final OnSwipeListener mOnSwipe; SwipeListeningOverlay(Context ctx, OnSwipeListener onSwipe) { super(ctx); mOnSwipe = onSwipe; } @Override protected void draw(Canvas c, MapView osmv, boolean shadow) { // Nothing to draw } @Override public boolean onScroll(final MotionEvent pEvent1, final MotionEvent pEvent2, final float pDistanceX, final float pDistanceY, final MapView pMapView) { if (mOnSwipe != null) { mOnSwipe.onSwipe(); } return false; } private static interface OnSwipeListener { public void onSwipe(); } } // Used to blank the high-res tile source when adding a low-res overlay private class BlankTileSource extends OnlineTileSourceBase { BlankTileSource() { super("fake", ResourceProxy.string.mapquest_aerial /* arbitrary value */, AbstractMapOverlay.getDisplaySizeBasedMinZoomLevel(), AbstractMapOverlay.MAX_ZOOM_LEVEL_OF_MAP, AbstractMapOverlay.TILE_PIXEL_SIZE, "", new String[]{""}); } @Override public String getTileURLString(MapTile aTile) { return null; } } private class CoverageSetup { private AtomicBoolean isGetUrlAndInitCoverageRunning = new AtomicBoolean(); private void initOnMainThread() { final Runnable runnable = new Runnable() { @Override public void run() { final ClientPrefs.MapTileResolutionOptions resolution = ClientPrefs.getInstance(mRootView.getContext()).getMapTileResolutionType(); if (mCoverageTilesOverlayLowZoom != null || // checks if init() has already happened resolution == ClientPrefs.MapTileResolutionOptions.NoMap) { return; } initCoverageTiles(sCoverageUrl); updateOverlayCoverageLayer(mMap.getZoomLevel()); } }; mMap.post(runnable); } void getUrlAndInit() { if (!isGetUrlAndInitCoverageRunning.compareAndSet(false, true)) { return; } final Runnable coverageUrlQuery = new Runnable() { @Override public void run() { if (sCoverageUrl != null) { initOnMainThread(); isGetUrlAndInitCoverageRunning.set(false); return; } mGetUrl.schedule(new TimerTask() { @Override public void run() { try { IHttpUtil httpUtil = (IHttpUtil) ServiceLocator.getInstance().getService(IHttpUtil.class); java.util.Scanner scanner = new java.util.Scanner(httpUtil.getUrlAsStream(COVERAGE_REDIRECT_URL), "UTF-8"); if (scanner.hasNext()) { scanner.useDelimiter("\\A"); String result = scanner.next(); try { sCoverageUrl = new JSONObject(result).getString("tiles_url"); removeLayer(mCoverageTilesOverlayHighZoom); removeLayer(mCoverageTilesOverlayLowZoom); mCoverageTilesOverlayHighZoom = mCoverageTilesOverlayLowZoom = null; } catch (JSONException ex) { AppGlobals.guiLogInfo("Failed to get coverage url: " + ex.toString()); } } scanner.close(); } catch (Exception ex) { // this will catch java.net.UnknownHostException when offline if (AppGlobals.isDebug) { ClientLog.d(LOG_TAG, ex.toString()); } } // always init coverage tiles // cached tiles will be shown even if sCoverageUrl == null initOnMainThread(); isGetUrlAndInitCoverageRunning.set(false); } }, 0); } }; mMap.post(coverageUrlQuery); } } }