com.keysolutions.meteorparties.PartyMapFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.keysolutions.meteorparties.PartyMapFragment.java

Source

/*
* (c)Copyright 2013 Ken Yee, KEY Enterprise Solutions 
*
* 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.keysolutions.meteorparties;

import java.util.HashMap;

import android.app.Activity;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.GoogleMap.OnInfoWindowClickListener;
import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.keysolutions.ddpclient.android.DDPBroadcastReceiver;
import com.keysolutions.ddpclient.android.DDPStateSingleton;

/**
 * A Google map fragment showing parties on a map.
 * <p>
 * Activities containing this fragment MUST implement the {@link Callbacks}
 * interface.
 */
public class PartyMapFragment extends SupportMapFragment {
    private final static String TAG = "PartyMapActivity";

    private static final float ZOOM_LEVEL = 15;

    /** reference to Google Maps object */
    private GoogleMap mMap;

    /** broadcast receiver */
    private BroadcastReceiver mReceiver;

    /** for storing selected party in bundle */
    private static final String STATE_SELECTED_PARTY = "selected_party";

    /** for looking up Party from marker */
    private HashMap<String, Party> mPartyMarkerMap;

    /**
     * The fragment's current callback object, which is notified of map party
     * clicks.
     */
    @SuppressWarnings("unused")
    private Callbacks mCallbacks = sDummyCallbacks;

    /**
     * A callback interface that all activities containing this fragment must
     * implement. This mechanism allows activities to be notified of item
     * selections.
     */
    public interface Callbacks {
        /**
         * Callback for when an item has been selected.
         */
        public void onPartySelected(String id, boolean clickThrough);

        /**
         * Callback to find out if our parent activity is in two pane mode for
         * handling marker click behavior
         */
        public boolean isTwoPaneMode();

        /**
         * Callback to update login buttons in onResume because action bar isn't recreated
         */
        public void updateLoginButtons();
    }

    /**
     * A dummy implementation of the {@link Callbacks} interface that does
     * nothing. Used only when this fragment is not attached to an activity.
     */
    private static Callbacks sDummyCallbacks = new Callbacks() {
        @Override
        public void onPartySelected(String id, boolean clickThrough) {
        }

        @Override
        public boolean isTwoPaneMode() {
            return false;
        }

        @Override
        public void updateLoginButtons() {
        }
    };

    /** called when this fragment is attached to an activity */
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        // Activities containing this fragment must implement its callbacks.
        if (!(activity instanceof Callbacks)) {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        // Reset the active callbacks interface to the dummy implementation.
        mCallbacks = sDummyCallbacks;
    }

    /**
     * called after the activity has been created so we can initialize the map
     */
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // restore selected party if any
        // NOTE: this doesn't work because if you use setRetainInstance(true) to let GoogleMaps
        // retain internal state so it doesn't have to re-init so much:
        // http://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack
        // http://stackoverflow.com/questions/15545214/android-using-savedinstancestate-with-fragments
        if (savedInstanceState != null) {
            MyApplication.setSelectedPartyId(savedInstanceState.getString(STATE_SELECTED_PARTY));
        }

        int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getActivity());
        if (status == ConnectionResult.SUCCESS) {
            GoogleMap mapFragment = getMap();
            if (mapFragment != null) {
                if (savedInstanceState != null) {
                    // restore selected party ID
                    MyApplication.setSelectedPartyId(savedInstanceState.getString(STATE_SELECTED_PARTY));
                    // Reincarnated activity. The obtained map is the same map
                    // instance in the previous activity life cycle.
                    // There is no need to reinitialize it if setRetainInstance is set.
                    // However, you still have to add all your listeners to it later.
                    mMap = getMap();
                } else {
                    // First incarnation of this activity.
                    // set retaininstance to minimize rotation time w/ google maps
                    /*this.setRetainInstance(true);*/
                }
                initMap();
            } else {
                Log.e(TAG, "Couldn't find Google map fragment!");
            }
        } else {
            Dialog dialog = GooglePlayServicesUtil.getErrorDialog(status, getActivity(), 42/*
                                                                                            * some random requestCode...value isn't
                                                                                            * important
                                                                                            */);
            dialog.show();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // persist the activated party item
        outState.putString(STATE_SELECTED_PARTY, MyApplication.getSelectedPartyId());
    }

    @Override
    public void onPause() {
        super.onPause();

        if (mReceiver != null) {
            // unhook the receiver
            LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
            mReceiver = null;
        }
        // unhook any map markers
        for (Party party : MyDDPState.getInstance().getParties().values()) {
            party.setMarker(null);
        }
    }

    @Override
    public void onResume() {
        super.onResume();

        // needed to update login buttons if on smartphone because the action bar
        // isn't refreshed when you come back to this activity after login activity
        ((Callbacks) getActivity()).updateLoginButtons();

        // re-hook or init map
        initMap();

        // get ready to handle DDP events
        mReceiver = new DDPBroadcastReceiver(MyDDPState.getInstance(), getActivity()) {
            @Override
            protected void onDDPConnect(DDPStateSingleton ddp) {
                super.onDDPConnect(ddp);
                // add our subscriptions needed for the activity here
                ddp.subscribe("parties", new Object[] {});
                ddp.subscribe("directory", new Object[] {});
            }

            @Override
            protected void onSubscriptionUpdate(String changeType, String subscriptionName, String docId) {
                if (subscriptionName.equals("parties")) {
                    // show any new parties
                    showVisibleParties();
                }
            }

            @Override
            protected void onLogin() {
                // update login/logout action button
                getActivity().invalidateOptionsMenu();
            }

            @Override
            protected void onLogout() {
                // update login/logout action button
                getActivity().invalidateOptionsMenu();
            }
        };
        MyDDPState.getInstance().connectIfNeeded(); // start connection process if we're not connected
    }

    /**
     * used to initialize the map
     */
    private void initMap() {
        if (mMap == null) {
            mMap = getMap();
            if (mMap != null) {
                initialMapSetup();
            }
        } else {
            hookMap();
        }
    }

    /**
     * initial setup of map
     */
    private void initialMapSetup() {
        hookMap();

        // zoom to current location if available, otherwise use camera
        // changelistener
        mMap.animateCamera(CameraUpdateFactory.zoomTo(ZOOM_LEVEL), 2000, null);

        // set map location to current location
        // Location curLoc = getCurrentLocation();
        // hardcode San Fran downtown to be compatible w/ Parties demo app
        Location curLoc = new Location("network");
        curLoc.setLatitude(37.78212);
        curLoc.setLongitude(-122.40146);
        if (curLoc != null) {
            mMap.moveCamera(CameraUpdateFactory
                    .newLatLngZoom(new LatLng(curLoc.getLatitude(), curLoc.getLongitude()), ZOOM_LEVEL));
        }
    }

    /**
     * Hooks all listeners into the map (needed if you rotate screen
     * or onResume)
     */
    public void hookMap() {
        mMap.setMyLocationEnabled(true);
        // handle user drags of map
        mMap.setOnCameraChangeListener(new OnCameraChangeListener() {
            @Override
            public void onCameraChange(CameraPosition position) {
                showVisibleParties();
            }
        });
        // handle user clicks on markers
        mMap.setOnMarkerClickListener(new OnMarkerClickListener() {
            @Override
            public boolean onMarkerClick(Marker marker) {
                Party party = mPartyMarkerMap.get(marker.getTitle() + marker.getSnippet());
                if (party != null) {
                    if (((PartyMapActivity) getActivity()).isTwoPaneMode()) {
                        // refreshes detail fragment
                        ((PartyMapActivity) getActivity()).onPartySelected(party.getId(), true);
                    }
                    MyApplication.setSelectedPartyId(party.getId());
                } else {
                    MyApplication.setSelectedPartyId(null);
                }

                return false; // false = move to map and show info
            }
        });
        // handle user clicks on infowindow (this is for phone screens to start
        // detail activity)
        mMap.setOnInfoWindowClickListener(new OnInfoWindowClickListener() {
            @Override
            public void onInfoWindowClick(Marker marker) {
                Party party = mPartyMarkerMap.get(marker.getTitle() + marker.getSnippet());
                if (party != null) {
                    ((PartyMapActivity) getActivity()).onPartySelected(party.getId(), true);
                    MyApplication.setSelectedPartyId(party.getId());
                }
            }
        });
    }

    /**
     * Displays all visible parties on map so we don't set up
     * excess markers and slow it down
     */
    private void showVisibleParties() {
        if (this.mMap == null) {
            // if Google Play Services is not installed, map will be null
            return;
        }
        if (MyApplication.getSelectedPartyId() == null) {
            // select the first party if nothing is selected yet
            if (MyDDPState.getInstance().getParties().size() > 0) {
                MyApplication.setSelectedPartyId(MyDDPState.getInstance().getParties().keySet().iterator().next());
            }
        }
        String partyId = MyApplication.getSelectedPartyId();
        // we have to do this convoluted marker-party hashmap because
        // the Google Maps API doesn't let us tuck an ID into the marker object
        // Note: you can use http://code.google.com/p/android-maps-extensions/
        // but this wasn't included to minimize project references
        if (mPartyMarkerMap == null) {
            mPartyMarkerMap = new HashMap<String, Party>();
        }
        mPartyMarkerMap.clear();
        Marker selectedMarker = null;
        // add all party locations that are visible on the current map
        LatLngBounds visibleBounds = this.mMap.getProjection().getVisibleRegion().latLngBounds;
        for (Party party : MyDDPState.getInstance().getParties().values()) {
            if (visibleBounds.contains(new LatLng(party.getLatitude(), party.getLongitude()))) {
                if (party.getMarker() == null) {
                    // add party if it wasn't visible
                    Marker marker = mMap.addMarker(
                            new MarkerOptions().position(new LatLng(party.getLatitude(), party.getLongitude()))
                                    .title(party.getTitle()).snippet(party.getDescription())
                                    .icon(BitmapDescriptorFactory.defaultMarker(party.getMarkerColor())));
                    party.setMarker(marker);
                }
                if (party.getId() == partyId) {
                    selectedMarker = party.getMarker();
                }
                mPartyMarkerMap.put(party.getMarker().getTitle() + party.getMarker().getSnippet(), party);
            } else {
                // remove any party that is not visible
                party.setMarker(null);
            }
        }
        // show marker info for selected marker
        if (selectedMarker != null) {
            selectedMarker.showInfoWindow();
            // also send fragment notification
            Party party = mPartyMarkerMap.get(selectedMarker.getTitle() + selectedMarker.getSnippet());
            if (party != null) {
                PartyMapActivity activity = ((PartyMapActivity) getActivity());
                // when rotating device, activity can be null for a small fraction of time
                if (activity != null) {
                    activity.onPartySelected(party.getId(), false);
                }
            }
        }
    }

    /**
     * Gets current location using Android's location provider
     * so you can zoom the map to it
     * @return last known Location
     */
    @SuppressWarnings("unused")
    private Location getCurrentLocation() {
        Criteria criteria = new Criteria();
        LocationManager locMan = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE);
        String towers = locMan.getBestProvider(criteria, false);
        Location location = locMan.getLastKnownLocation(towers);
        return location;
    }
}