/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.samples.apps.iosched.ui; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; import static com.google.samples.apps.iosched.util.LogUtils.LOGE; import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag; import android.annotation.SuppressLint; import android.app.Activity; import android.app.DialogFragment; import android.content.*; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Point; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.app.LoaderManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.CursorLoader; import android.content.Loader; import android.text.format.DateUtils; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import com.google.android.gms.maps.CameraUpdate; import com.google.samples.apps.iosched.R; import com.google.samples.apps.iosched.provider.ScheduleContract; import com.google.samples.apps.iosched.util.*; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.Projection; import com.google.android.gms.maps.model.*; import com.google.android.gms.maps.model.Marker; import com.google.maps.android.ui.IconGenerator; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Formatter; import java.util.HashMap; import java.util.Locale; import com.jakewharton.disklrucache.DiskLruCache; /** * Shows a map of the conference venue. */ public class MapFragment extends com.google.android.gms.maps.MapFragment implements GoogleMap.OnInfoWindowClickListener, GoogleMap.OnMarkerClickListener, GoogleMap.OnIndoorStateChangeListener, LoaderCallbacks<Cursor>, GoogleMap.OnMapLoadedCallback { private static final LatLng MOSCONE = new LatLng(37.783107, -122.403789); private static final LatLng MOSCONE_CAMERA = new LatLng(37.78308931536713, -122.40409433841705); // Initial camera zoom private static final float CAMERA_ZOOM = 18.19f; private static final float CAMERA_BEARING = 234.2f; private static final int INVALID_FLOOR = Integer.MIN_VALUE; // Estimated number of floors used to initialise data structures with appropriate capacity private static final int INITIAL_FLOOR_COUNT = 3; // Default level (index of level in IndoorBuilding object for Moscone) private static final int MOSCONE_DEFAULT_LEVEL_INDEX = 1; /** * When specified, will automatically point the map to the requested room. */ public static final String EXTRA_ROOM = "com.google.android.iosched.extra.ROOM"; private static final String TAG = makeLogTag(MapFragment.class); // Marker types public static final String TYPE_SESSION = "session"; public static final String TYPE_PARTNER = "partner"; public static final String TYPE_PLAIN_SESSION = "plainsession"; public static final String TYPE_LABEL = "label"; public static final String TYPE_MOSCONE = "moscone"; public static final String TYPE_INACTIVE = "inactive"; // Tile Providers private SparseArray<TileProvider> mTileProviders = new SparseArray<TileProvider>(INITIAL_FLOOR_COUNT); private SparseArray<TileOverlay> mTileOverlays = new SparseArray<TileOverlay>(INITIAL_FLOOR_COUNT); private DiskLruCache mTileCache; // Markers stored by id private HashMap<String, MarkerModel> mMarkers = new HashMap<String, MarkerModel>(); // Markers stored by floor private SparseArray<ArrayList<Marker>> mMarkersFloor = new SparseArray<ArrayList<Marker>>(INITIAL_FLOOR_COUNT); private boolean mOverlaysLoaded = false; private boolean mMarkersLoaded = false; // Cached size of view private int mWidth, mHeight; // Padding for #centerMap private int mShiftRight = 0; private int mShiftTop = 0; // Screen DPI private float mDPI = 0; // Indoor maps representation of Moscone Center private IndoorBuilding mMosconeBuilding = null; // currently displayed floor private int mFloor = INVALID_FLOOR; // Show markers at default zoom level private boolean mShowMarkers = true; private boolean mAtMoscone = false; private Marker mMosconeMaker = null; private GoogleMap mMap; private MapInfoWindowAdapter mInfoAdapter; private IconGenerator mIconGenerator; // Handler for info window queries private AsyncQueryHandler mQueryHandler; //For Analytics tracking public static final String SCREEN_LABEL = "Map"; public interface Callbacks { public void onSessionRoomSelected(String roomId, String roomTitle); public void onShowPartners(); } private static Callbacks sDummyCallbacks = new Callbacks() { @Override public void onSessionRoomSelected(String roomId, String roomTitle) { } @Override public void onShowPartners() {} }; private Callbacks mCallbacks = sDummyCallbacks; private String mHighlightedRoom = null; public static MapFragment newInstance() { return new MapFragment(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /* [ANALYTICS:SCREEN] * TRIGGER: View the Map screen. * LABEL: 'Map' * [/ANALYTICS] */ AnalyticsManager.sendScreenView(SCREEN_LABEL); // get DPI mDPI = getActivity().getResources().getDisplayMetrics().densityDpi / 160f; // setup the query handler to populate info windows mQueryHandler = createInfowindowHandler(getActivity().getContentResolver()); mIconGenerator = new IconGenerator(getActivity()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View mapView = super.onCreateView(inflater, container, savedInstanceState); View v = inflater.inflate(R.layout.fragment_map, container, false); FrameLayout layout = (FrameLayout) v.findViewById(R.id.map_container); layout.addView(mapView, 0); // get the height and width of the view mapView.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @SuppressWarnings("deprecation") @SuppressLint("NewApi") @Override public void onGlobalLayout() { final View v = getView(); mHeight = v.getHeight(); mWidth = v.getWidth(); // also requires width and height enableMapElements(); if (v.getViewTreeObserver().isAlive()) { // remove this layout listener if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { v.getViewTreeObserver() .removeOnGlobalLayoutListener(this); } else { v.getViewTreeObserver() .removeGlobalOnLayoutListener(this); } } } } ); clearMap(); if (mMap == null) { setupMap(true); } mMap.setPadding(0, UIUtils.calculateActionBarSize(getActivity()), 0, 0); // load all markers LoaderManager lm = getLoaderManager(); lm.initLoader(MarkerQuery._TOKEN, null, this); // load the tile overlays lm.initLoader(OverlayQuery._TOKEN, null, this); return v; } @Override public void onStart() { super.onStart(); // Open tile disk cache mTileCache = MapUtils.openDiskCache(getActivity()); } @Override public void onStop() { super.onStop(); if (mTileCache != null) { try { mTileCache.close(); } catch (IOException e) { // Ignore } } } /** * Clears the map and initialises all map variables that hold markers and overlays. */ private void clearMap() { if (mMap != null) { mMap.clear(); } // Clear all map elements mTileProviders.clear(); mTileOverlays.clear(); mMarkers.clear(); mMarkersFloor.clear(); mFloor = INVALID_FLOOR; } private void setupMap(boolean resetCamera) { mInfoAdapter = new MapInfoWindowAdapter(LayoutInflater.from(getActivity()), getResources(), mMarkers); mMap = getMap(); // Add a Marker for Moscone mMosconeMaker = mMap.addMarker(MapUtils.createMosconeMarker(mIconGenerator, MOSCONE, getActivity()).visible(false)); mMap.setOnMarkerClickListener(this); mMap.setOnInfoWindowClickListener(this); mMap.setOnIndoorStateChangeListener(this); mMap.setOnMapLoadedCallback(this); mMap.setInfoWindowAdapter(mInfoAdapter); if (resetCamera) { // Move camera directly to Moscone centerOnMoscone(false); } mMap.setIndoorEnabled(false); mMap.getUiSettings().setZoomControlsEnabled(false); mMap.setMyLocationEnabled(false); Bundle data = getArguments(); if (data != null && data.containsKey(EXTRA_ROOM)) { mHighlightedRoom = data.getString(EXTRA_ROOM); } LOGD(TAG, "Map setup complete."); } @Override public void onMapLoaded() { // Enable indoor maps once the map has loaded for the first time // Workaround for issue where floor picker is not displayed for the first active building mMap.setIndoorEnabled(true); } @Override public void onAttach(Activity activity) { super.onAttach(activity); if (!(activity instanceof Callbacks)) { throw new ClassCastException( "Activity must implement fragment's callbacks."); } mCallbacks = (Callbacks) activity; activity.getContentResolver().registerContentObserver( ScheduleContract.MapMarkers.CONTENT_URI, true, mObserver); activity.getContentResolver().registerContentObserver( ScheduleContract.MapTiles.CONTENT_URI, true, mObserver); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(activity); } @Override public void onDetach() { super.onDetach(); mCallbacks = sDummyCallbacks; getActivity().getContentResolver().unregisterContentObserver(mObserver); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity()); } /** * Moves the camera to Moscone Center (as defined in {@link #MOSCONE} and {@link #CAMERA_ZOOM}. * @param animate Animates the camera if true, otherwise it is moved */ private void centerOnMoscone(boolean animate) { CameraUpdate camera = CameraUpdateFactory.newCameraPosition( new CameraPosition.Builder().bearing(CAMERA_BEARING).target(MOSCONE_CAMERA).zoom(CAMERA_ZOOM).tilt(0f).build()); if (animate) { mMap.animateCamera(camera); } else { mMap.moveCamera(camera); } } /** * Switches the displayed floor for which elements are displayed. * If the map is not initialised yet or no data has been loaded, nothing will be displayed. * If an invalid floor is specified and elements are currently on the map, all visible * elements will be hidden. * If this floor is not active for the indoor building, it is made active. * * @param floor index of the floor to display. It requires an overlay and at least one Marker to * be defined for it and it has to be a valid index in the * {@link com.google.android.gms.maps.model.IndoorBuilding} object that * describes Moscone. */ private void showFloorElementsIndex(int floor) { LOGD(TAG, "Show floor " + floor); if (mFloor == floor) { return; } // Hide previous floor elements if it is valid if (isValidFloor(mFloor)) { setFloorElementsVisible(mFloor, false); } mFloor = floor; if (isValidFloor(mFloor) && mAtMoscone) { // Always hide the Moscone marker if a floor is shown mMosconeMaker.setVisible(false); setFloorElementsVisible(mFloor, true); } else { // Show Moscone marker if this is not a valid floor mMosconeMaker.setVisible(true); } } /** * Change the active floor of Moscone Center * to the given floor index. See {@link #showFloorElementsIndex(int)}. * * @param floor Index of the floor to show. * @see #showFloorElementsIndex(int) */ private void showFloorIndex(int floor) { if (isValidFloor(floor) && mAtMoscone) { if (mMap.getFocusedBuilding().getActiveLevelIndex() == floor) { // This floor is already active, show its elements showFloorElementsIndex(floor); } else { // This floor is not shown yet, switch to this floor on the map mMap.getFocusedBuilding().getLevels().get(floor).activate(); } } else { LOGD(TAG, "Can't show floor index " + floor + "."); } } /** * Change the visibility of all Markers and TileOverlays for a floor. * * @param floor * @param visible */ private void setFloorElementsVisible(int floor, boolean visible) { // Overlays final TileOverlay overlay = mTileOverlays.get(floor); if (overlay != null) { overlay.setVisible(visible); } // Markers final ArrayList<Marker> markers = mMarkersFloor.get(floor); if (markers != null) { for (Marker m : markers) { m.setVisible(visible); } } } /** * A floor is valid if it has tiles, overlays and the Moscone building contains that floor. * * @param floor * @return */ private boolean isValidFloor(int floor) { return mMarkersFloor.get(floor) != null && mTileOverlays.get(floor) != null && floor < mMosconeBuilding.getLevels().size(); } /** * Display map features when all loaders have finished. * This ensures that only complete data for the correct floor is shown. */ private void enableMapElements() { if (mOverlaysLoaded && mMarkersLoaded && mWidth > 0 && mHeight > 0 && mMosconeBuilding != null) { // Enable indoor map again if data has been reloaded // Tiles and markers are enabled in the indoor API callbacks mMap.setIndoorEnabled(true); // If already focused on Moscone, show the elements for the active floor // (The indoor callbacks will not be received in this case.) if(mAtMoscone){ showFloorIndex(mMosconeBuilding.getActiveLevelIndex()); } } } private void onDefocusMoscone() { // Hide all markers and tile overlays showFloorElementsIndex(INVALID_FLOOR); } private void onFocusMoscone() { // Highlight a room if argument is set and it exists, otherwise show the default floor if (mHighlightedRoom != null && mMarkers.containsKey(mHighlightedRoom)) { highlightRoom(mHighlightedRoom); mHighlightedRoom = null; } else { // Switch to the default level for Moscone showFloorIndex(MOSCONE_DEFAULT_LEVEL_INDEX); } } @Override public void onIndoorBuildingFocused() { IndoorBuilding building = mMap.getFocusedBuilding(); if (building != null && mMosconeBuilding == null && mMap.getProjection().getVisibleRegion().latLngBounds.contains(MOSCONE)) { // Store the first active building. This will always be Moscone mMosconeBuilding = building; enableMapElements(); } if (building != null && mMosconeBuilding.equals(building)) { // Map is focused on Moscone Center mAtMoscone = true; onFocusMoscone(); } else if(mAtMoscone){ // Map is no longer focused on Moscone Center mAtMoscone = false; onDefocusMoscone(); } } @Override public void onIndoorLevelActivated(IndoorBuilding indoorBuilding) { if (indoorBuilding.equals(mMosconeBuilding)) { // Show map elements for this floor if at Moscone showFloorElementsIndex(indoorBuilding.getActiveLevelIndex()); } } void addTileProvider(int floor, File f) { if (!f.exists()) { return; } TileProvider provider; try { SVGTileProvider svgProvider = new SVGTileProvider(f, mDPI); if (mTileCache == null) { // Use the SVGTileProvider directly as the TileProvider without a cache provider = svgProvider; } else { // Wrap the SVGTileProvider ina a CachedTileProvider for caching on disk provider = new CachedTileProvider(Integer.toString(floor), svgProvider, mTileCache); } } catch (IOException e) { LOGD(TAG, "Could not create Tile Provider."); return; } TileOverlayOptions tileOverlay = new TileOverlayOptions() .tileProvider(provider).visible(false); mTileProviders.put(floor, provider); mTileOverlays.put(floor, mMap.addTileOverlay(tileOverlay)); } @Override public void onInfoWindowClick(Marker marker) { final String snippet = marker.getSnippet(); final String title = marker.getTitle(); // Log clicks on session and partner info windows if (TYPE_SESSION.equals(snippet) || TYPE_PARTNER.equals(snippet)) { /* [ANALYTICS:EVENT] * TRIGGER: Click on a pin that represents a room on the map. * CATEGORY: 'Map' * ACTION: 'infoclick' * LABEL: room ID (for example "Room_10") * [/ANALYTICS] */ AnalyticsManager.sendEvent("Map", "infoclick", title, 0L); } if (TYPE_SESSION.equals(snippet)) { mCallbacks.onSessionRoomSelected(title, mMarkers.get(title).label); } } @Override public boolean onMarkerClick(Marker marker) { final String snippet = marker.getSnippet(); final String title = marker.getTitle(); // Log clicks on session and partner markers if (TYPE_SESSION.equals(snippet) || TYPE_PARTNER.equals(snippet)) { /* [ANALYTICS:EVENT] * TRIGGER: Click on a marker on the map. * CATEGORY: 'Map' * ACTION: 'markerclick' * LABEL: marker ID (for example room UUID or partner marker id) * [/ANALYTICS] */ AnalyticsManager.sendEvent("Map", "markerclick", title, 0L); } if(marker.equals(mMosconeMaker)){ // Return camera to Moscone LOGD(TAG, "Clicked on Moscone marker, return to initial display."); centerOnMoscone(true); } else if (TYPE_SESSION.equals(snippet)) { final long time = UIUtils.getCurrentTime(getActivity()); Uri uri = ScheduleContract.Sessions.buildSessionsInRoomAfterUri(title, time); final String order = ScheduleContract.Sessions.SESSION_START + " ASC"; mQueryHandler.startQuery(SessionAfterQuery._TOKEN, title, uri, SessionAfterQuery.PROJECTION, null, null, order); } else if (TYPE_PARTNER.equals(snippet)) { mCallbacks.onShowPartners(); } else if (TYPE_PLAIN_SESSION.equals(snippet)) { // Show a basic info window with a title only marker.showInfoWindow(); } // ignore other markers //centerMap(marker.getPosition()); return true; } private void centerMap(LatLng position) { // calculate the new center of the map, taking into account optional // padding Projection proj = mMap.getProjection(); Point p = proj.toScreenLocation(position); // apply padding p.x = (int) (p.x - Math.round(mWidth * 0.5)) + mShiftRight; p.y = (int) (p.y - Math.round(mHeight * 0.5)) + mShiftTop; mMap.animateCamera(CameraUpdateFactory.scrollBy(p.x, p.y)); } /** * Set the padding around centered markers. Specified in the percentage of * the screen space of the map. */ public void setCenterPadding(float xFraction, float yFraction) { int oldShiftRight = mShiftRight; int oldShiftTop = mShiftTop; mShiftRight = Math.round(xFraction * mWidth); mShiftTop = Math.round(yFraction * mWidth); // re-center the map, shift displayed map by x and y fraction if map is // ready if (mMap != null) { mMap.animateCamera(CameraUpdateFactory.scrollBy(mShiftRight - oldShiftRight, mShiftTop - oldShiftTop)); } } private void highlightRoom(String roomId) { MarkerModel m = mMarkers.get(roomId); if (m != null) { showFloorIndex(m.floor); // explicitly show the marker before info window is shown. m.marker.setVisible(true); onMarkerClick(m.marker); centerMap(m.marker.getPosition()); } } /** * Create an {@link AsyncQueryHandler} for use with the * {@link MapInfoWindowAdapter}. */ private AsyncQueryHandler createInfowindowHandler(ContentResolver contentResolver) { return new AsyncQueryHandler(contentResolver) { StringBuilder mBuffer = new StringBuilder(); Formatter mFormatter = new Formatter(mBuffer, Locale.getDefault()); @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { MarkerModel model = mMarkers.get(cookie); mInfoAdapter.clearData(); if (model == null || cursor == null) { // query did not complete or incorrect data was loaded return; } final long time = UIUtils.getCurrentTime(getActivity()); switch (token) { case SessionAfterQuery._TOKEN: { extractSession(cursor, model, time); } break; } // update the displayed window model.marker.showInfoWindow(); } private static final long SHOW_UPCOMING_TIME = 24 * 60 * 60 * 1000; // 24 hours private void extractSession(Cursor cursor, MarkerModel model, long time) { if (cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); String currentTitle = null; String nextTitle = null; String nextTime = null; final long blockStart = cursor.getLong(SessionAfterQuery.SESSION_START); final long blockEnd = cursor.getLong(SessionAfterQuery.SESSION_END); boolean inProgress = time >= blockStart && time <= blockEnd; if (inProgress) { // A session is running, display its name and optionally // the next session currentTitle = cursor.getString(SessionAfterQuery.SESSION_TITLE); //move to the next entry cursor.moveToNext(); } if (!cursor.isAfterLast()) { //There is a session coming up next, display only it if it's within 24 hours of the current time final long nextStart = cursor.getLong(SessionAfterQuery.SESSION_START); if (nextStart < time + SHOW_UPCOMING_TIME) { nextTitle = cursor.getString(SessionAfterQuery.SESSION_TITLE); mBuffer.setLength(0); boolean showWeekday = !DateUtils.isToday(blockStart) && !UIUtils.isSameDayDisplay(UIUtils.getCurrentTime(getActivity()), blockStart, getActivity()); nextTime = DateUtils.formatDateRange(getActivity(), mFormatter, nextStart, nextStart, DateUtils.FORMAT_SHOW_TIME | (showWeekday ? DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY : 0), PrefUtils.getDisplayTimeZone(getActivity()).getID() ).toString(); } } // populate the info window adapter mInfoAdapter.setSessionData(model.marker, model.label, currentTitle, nextTitle, nextTime, inProgress); } else { // No entries, display name of room only mInfoAdapter.setMarker(model.marker, model.label); } } }; } // Loaders private void onMarkerLoaderComplete(Cursor cursor) { if (cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); while (!cursor.isAfterLast()) { // get data final String id = cursor.getString(MarkerQuery.MARKER_ID); final int floor = cursor.getInt(MarkerQuery.MARKER_FLOOR); final float lat = cursor.getFloat(MarkerQuery.MARKER_LATITUDE); final float lon = cursor.getFloat(MarkerQuery.MARKER_LONGITUDE); final String type = cursor.getString(MarkerQuery.MARKER_TYPE); final String label = cursor.getString(MarkerQuery.MARKER_LABEL); final LatLng position = new LatLng(lat, lon); MarkerOptions marker = null; if (TYPE_SESSION.equals(type) || TYPE_PLAIN_SESSION.equals(type)) { marker = MapUtils.createSessionMarker(id, type, position); } else if (TYPE_PARTNER.equals(type)) { marker = MapUtils.createPartnerMarker(id, position); } else if (TYPE_LABEL.equals(type)) { marker = MapUtils.createLabelMarker(mIconGenerator, id, position, label); } // add marker to map if (marker != null) { Marker m = mMap.addMarker(marker); MarkerModel model = new MarkerModel(id, floor, type, label, m); ArrayList<Marker> markerList = mMarkersFloor.get(floor); if (markerList == null) { // Initialise the list of Markers for this floor markerList = new ArrayList<Marker>(); mMarkersFloor.put(floor, markerList); } markerList.add(m); mMarkers.put(id, model); } cursor.moveToNext(); } // no more markers to load mMarkersLoaded = true; enableMapElements(); } } private void onOverlayLoaderComplete(Cursor cursor) { if (cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); while (!cursor.isAfterLast()) { final int floor = cursor.getInt(OverlayQuery.TILE_FLOOR); final String file = cursor.getString(OverlayQuery.TILE_FILE); File f = MapUtils.getTileFile(getActivity().getApplicationContext(), file); if (f != null) { addTileProvider(floor, f); } cursor.moveToNext(); } } mOverlaysLoaded = true; enableMapElements(); } private interface MarkerQuery { int _TOKEN = 0x1; String[] PROJECTION = { ScheduleContract.MapMarkers.MARKER_ID, ScheduleContract.MapMarkers.MARKER_FLOOR, ScheduleContract.MapMarkers.MARKER_LATITUDE, ScheduleContract.MapMarkers.MARKER_LONGITUDE, ScheduleContract.MapMarkers.MARKER_TYPE, ScheduleContract.MapMarkers.MARKER_LABEL }; int MARKER_ID = 0; int MARKER_FLOOR = 1; int MARKER_LATITUDE = 2; int MARKER_LONGITUDE = 3; int MARKER_TYPE = 4; int MARKER_LABEL = 5; } private interface OverlayQuery { int _TOKEN = 0x3; String[] PROJECTION = { ScheduleContract.MapTiles.TILE_FLOOR, ScheduleContract.MapTiles.TILE_FILE }; int TILE_FLOOR = 0; int TILE_FILE = 1; } private interface SessionAfterQuery { int _TOKEN = 0x5; String[] PROJECTION = { ScheduleContract.Sessions.SESSION_TITLE, ScheduleContract.Sessions.SESSION_START, ScheduleContract.Sessions.SESSION_END, ScheduleContract.Rooms.ROOM_NAME }; int SESSION_TITLE = 0; int SESSION_START = 1; int SESSION_END = 2; int ROOM_NAME = 3; } @Override public Loader<Cursor> onCreateLoader(int id, Bundle data) { switch (id) { case MarkerQuery._TOKEN: { Uri uri = ScheduleContract.MapMarkers.buildMarkerUri(); return new CursorLoader(getActivity(), uri, MarkerQuery.PROJECTION, null, null, null); } case OverlayQuery._TOKEN: { Uri uri = ScheduleContract.MapTiles.buildUri(); return new CursorLoader(getActivity(), uri, OverlayQuery.PROJECTION, null, null, null); } } return null; } @Override public void onLoaderReset(Loader<Cursor> arg0) { } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { if (getActivity() == null) { return; } switch (loader.getId()) { case MarkerQuery._TOKEN: onMarkerLoaderComplete(cursor); break; case OverlayQuery._TOKEN: onOverlayLoaderComplete(cursor); break; } } private final ContentObserver mObserver = new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange) { if (!isAdded()) { return; } //clear map reload all data clearMap(); setupMap(false); // reload data from loaders LoaderManager lm = getActivity().getLoaderManager(); mMarkersLoaded = false; mOverlaysLoaded = false; Loader<Cursor> loader = lm.getLoader(MarkerQuery._TOKEN); if (loader != null) { loader.forceLoad(); } loader = lm.getLoader(OverlayQuery._TOKEN); if (loader != null) { loader.forceLoad(); } } }; /** * A structure to store information about a Marker. */ public static class MarkerModel { String id; int floor; String type; String label; Marker marker; public MarkerModel(String id, int floor, String type, String label, Marker marker) { this.id = id; this.floor = floor; this.type = type; this.label = label; this.marker = marker; } } }