com.yayandroid.utility.MapHelperFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.yayandroid.utility.MapHelperFragment.java

Source

/**
 * 
 */
package com.yayandroid.utility;

import java.util.List;
import java.util.Locale;

import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.json.JSONArray;
import org.json.JSONObject;

import android.annotation.SuppressLint;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.net.http.AndroidHttpClient;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.Window;
import android.widget.FrameLayout;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;

/**
 * @author y.bayramoglu
 * 
 */
public class MapHelperFragment extends Fragment {

    private final int CHECK_MY_LOCATION_DELAY = 500;
    private final int REQUEST_CODE_PLAY_SERVICE = 111;
    private final int MAP_ZOOM_LEVEL = 10;
    private final String PROGRESS_MESSAGE_GETTING_LOCATION = "Getting location...";
    private final String TAG_ERROR_DIALOG_FRAGMENT = "Error_Dialog";

    public GoogleMap mapView;
    public Location myLocation;

    private ProgressDialog progressDialog;
    private View progressView;
    private FrameLayout mapHost;
    private SupportMapFragment mapFragment;
    private Handler delayHandler;
    private boolean progressDialogCancelable = true;
    private boolean shouldMoveToMyLocation = false;
    private boolean runningThreadCheckMap = false;
    private boolean runningThreadGetMyLocation = false;
    private boolean runningThreadGetLocationInfo = false;
    private LocInfoHolder[] requiredInformations;

    /**
     * Enumeration to determine which value is onLocationInfoReceived method. In
     * this enumeration values may be needed to change according to Country
     * address standards.
     * 
     * @author y.bayramoglu
     * 
     */
    public enum LocInfoType {

        ROUTE("route"), NEIGHBORHOOD("neighborhood"), LOCALITY("locality"), COUNTY(
                "administrative_area_level_2"), CITY("administrative_area_level_1"), COUNTRY("country");

        public final String value;

        private LocInfoType(String value) {
            this.value = value;
        }

    }

    /**
     * Holder class to keep information about a locInfoType has been sent or not
     * 
     * @author y.bayramoglu
     * 
     */
    private class LocInfoHolder {

        public boolean hasSent = false;
        public LocInfoType type;

        public LocInfoHolder(LocInfoType type) {
            this.type = type;
        }

        @Override
        public boolean equals(Object o) {
            return type == ((LocInfoHolder) o).type;
        }

    }

    /**
     * Optional: But if you set this view, its visibility will be set to gone
     * when mapView has received
     */
    public void setProgressView(View progress) {
        this.progressView = progress;
    }

    /**
     * If progressView has also been set, then this view's visibility will be
     * set to visible when mapView has received. Otherwise it will not be used
     */
    public void setMapHost(FrameLayout mapHost) {
        this.mapHost = mapHost;
        if (mapHost != null) {
            mapHost.requestTransparentRegion(mapHost);
        }
    }

    /**
     * This method needs to be set as latest, because this method launches a
     * thread, which is intended to find mapView and to notify when it is able
     * or unable to find one
     */
    public void setMapFragment(SupportMapFragment mapFrag) {
        this.mapFragment = mapFrag;
        this.delayHandler = new Handler();
        CheckMap();
    }

    /**
     * Changes cancelable attribute of progressDialog, by default it is true
     */
    public void setProgressDialogCancelable(boolean cancelable) {
        this.progressDialogCancelable = cancelable;
        if (progressDialog != null)
            progressDialog.setCancelable(cancelable);
    }

    /**
     * This method needs to be called onMapReady, otherwise it will throw
     * nullPointerException because of map is not initialized yet. It animates
     * map to myLocation whenever it is detected.
     */
    public void moveToMyLocation(boolean shouldDisplayProgressDialog) {
        shouldMoveToMyLocation = true;
        GetMyLocation(shouldDisplayProgressDialog);
    }

    /**
     * This method needs to be called onMapReady, otherwise it will throw
     * nullPointerException because of map is not initialized yet. It starts a
     * thread which looks for myLocation, and calls onMyLocationReady statement
     * when it finds.
     */
    public void getMyLocation(boolean shouldDisplayProgressDialog) {
        shouldMoveToMyLocation = false;
        GetMyLocation(shouldDisplayProgressDialog);
    }

    /**
     * Trigger method to start a thread which tries to get user's current city
     * name, and posts onLocationInfoReceived method
     */
    public void getlocationInfo(LocInfoType... types) {
        this.requiredInformations = new LocInfoHolder[types.length];
        for (int i = 0; i < types.length; i++) {
            requiredInformations[i] = new LocInfoHolder(types[i]);
        }
        GetLocationInfo();
    }

    /**
     * Returns true if there is already found mapView, false otherwise
     */
    public boolean isMapReady() {
        return mapView != null;
    }

    /**
     * This method should be override to do mapView settings, it will be called
     * automatically after setMapFragment method received supportMapFragment and
     * if mapView has been found
     */
    public void onMapReady() {
        if (progressView != null) {
            progressView.setVisibility(View.GONE);
            if (mapHost != null) {
                mapHost.setVisibility(View.VISIBLE);
            }
        }
    }

    /**
     * This method should be override to display user error that there was no
     * available MapView, it will be called automatically after setMapFragment
     * method received supportMapFragment
     */
    public void noMapFound(boolean errorAlreadyDisplayed) {
        if (progressView != null) {
            progressView.setVisibility(View.GONE);
        }
    }

    /**
     * This method should be override if myLocation is necessary, and after
     * calling super method myLocation can be used in any purpose
     */
    public void onMyLocationReady() {
        if (shouldMoveToMyLocation) {
            CameraUpdate cameraUpdate = CameraUpdateFactory
                    .newLatLngZoom(new LatLng(myLocation.getLatitude(), myLocation.getLongitude()), MAP_ZOOM_LEVEL);
            mapView.animateCamera(cameraUpdate);
        }

        DismissProgressDialog();
    }

    /**
     * If any of information about user's current location then this method
     * needs to override, because when getlocationInfo method is called it will
     * push data to this method with locInfoType.
     */
    public void onLocationInfoReceived(LocInfoType locInfo, String locValue) {

    }

    @Override
    public void onSaveInstanceState(Bundle outState) {

        /**
         * Here onDestroyView we need to remove supportMapFragment, otherwise
         * when recalling this fragment will cause crash because of something
         * like mapFragment is not available. Little bit tricky.
         * 
         * I was removing mapFragment onDestroyView method but it started giving
         * "you can't call fragmentTransaction.commit() after onSaveInstanceState"
         * error, so i moved code here.
         */
        if (mapFragment != null) {
            FragmentTransaction fragmentTransaction = getActivity().getSupportFragmentManager().beginTransaction();
            fragmentTransaction.remove(mapFragment);
            fragmentTransaction.commit();
        }

        super.onSaveInstanceState(outState);
    }

    private void DismissProgressDialog() {
        if (progressDialog != null && progressDialog.isShowing())
            progressDialog.dismiss();
    }

    private void ShowProgressDialog(String message) {
        if (progressDialog == null) {
            progressDialog = new ProgressDialog(getActivity());
            progressDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
            progressDialog.setCancelable(progressDialogCancelable);
        }
        progressDialog.setMessage(message);
        if (getActivity() != null)
            progressDialog.show();
    }

    /**
     * Waits until user's current location gets found, up to given flag it
     * displays progressDialog to user or not. Here also can be display user
     * some notification about "please turn on gps/wifi" or something.
     */
    private void GetMyLocation(boolean shouldDisplayProgressDialog) {
        if (runningThreadGetMyLocation)
            return;

        if (shouldDisplayProgressDialog)
            ShowProgressDialog(PROGRESS_MESSAGE_GETTING_LOCATION);

        mapView.setMyLocationEnabled(true);

        new Thread(new Runnable() {

            @Override
            public void run() {
                runningThreadGetMyLocation = true;

                delayHandler.post(new Runnable() {

                    @Override
                    public void run() {
                        myLocation = mapView.getMyLocation();
                        if (myLocation == null) {
                            delayHandler.postDelayed(this, CHECK_MY_LOCATION_DELAY);
                        } else {
                            if (getActivity() != null) {
                                getActivity().runOnUiThread(new Runnable() {
                                    @Override
                                    public void run() {
                                        onMyLocationReady();
                                    }
                                });
                            }
                            runningThreadGetMyLocation = false;
                        }
                    }

                });

            }

        }).start();

    }

    /**
     * After getting myLocation, this method tries to get user's current
     * address. First with GeoCoder, if it cannot then it looks up googleApis to
     * get address online and parseS it.
     */
    private void GetLocationInfo() {
        if (runningThreadGetLocationInfo || myLocation == null)
            return;

        new Thread(new Runnable() {

            @SuppressLint("NewApi")
            @Override
            public void run() {
                runningThreadGetLocationInfo = true;

                Address address = null;

                if (Build.VERSION_CODES.FROYO < Build.VERSION.SDK_INT) {
                    if (Geocoder.isPresent()) {
                        try {
                            if (getActivity() != null) {
                                Geocoder geocoder = new Geocoder(getActivity(), Locale.getDefault());
                                List<Address> addresses = geocoder.getFromLocation(myLocation.getLatitude(),
                                        myLocation.getLongitude(), 1);
                                if (addresses.size() > 0) {
                                    address = addresses.get(0);
                                }
                            }
                        } catch (Exception ignored) {
                            /*
                             * After a while, GeoCoder start to throw
                             * "Service not available" exception. really weird
                             * since it was working before (same device, same
                             * Android version etc..)
                             */
                        }
                    }
                }

                if (address != null) {
                    // i.e., GeoCoder success
                    parseAddressInformation(address);
                } else {
                    // i.e., GeoCoder failed
                    fetchInformationUsingGoogleMap();
                }

                runningThreadGetLocationInfo = false;
            }

        }).start();
    }

    /**
     * Parses found address object from GeoCoder, and posts informations
     */
    private void parseAddressInformation(Address address) {

        /**
         * This parsing also may need to be modified up to your country's
         * addressing system
         */

        String value = address.getSubLocality();
        if (value != null)
            PostInformation(LocInfoType.COUNTY, value);

        value = address.getLocality();
        if (value != null)
            PostInformation(LocInfoType.LOCALITY, value);

        value = address.getAdminArea();
        if (value != null)
            PostInformation(LocInfoType.CITY, value);

        value = address.getCountryName();
        if (value != null)
            PostInformation(LocInfoType.COUNTRY, value);

        // If there is still some fields to get information about, then post
        // googleMap api to get them
        boolean isThereAnyLeft = false;
        for (int i = 0; i < requiredInformations.length; i++) {
            if (!requiredInformations[i].hasSent) {
                isThereAnyLeft = true;
            }
        }

        if (isThereAnyLeft)
            fetchInformationUsingGoogleMap();

    }

    /**
     * GoogleApis returns an address as jsonObject according to given
     * coordinate, and here we parse it to find necessary fields
     */
    private void fetchInformationUsingGoogleMap() {

        final AndroidHttpClient ANDROID_HTTP_CLIENT = AndroidHttpClient
                .newInstance(MapHelperFragment.class.getName());
        String googleMapUrl = "http://maps.googleapis.com/maps/api/geocode/json?latlng=" + myLocation.getLatitude()
                + "," + myLocation.getLongitude() + "&sensor=false&language=tr";
        try {
            JSONObject googleMapResponse = new JSONObject(
                    ANDROID_HTTP_CLIENT.execute(new HttpGet(googleMapUrl), new BasicResponseHandler()));

            // many nested loops.. not great -> use expression instead
            // loop among all results
            JSONArray results = (JSONArray) googleMapResponse.get("results");
            for (int i = 0; i < results.length(); i++) {
                // loop among all addresses within this result
                JSONObject result = results.getJSONObject(i);
                if (result.has("address_components")) {
                    JSONArray addressComponents = result.getJSONArray("address_components");

                    for (int j = 0; j < addressComponents.length(); j++) {
                        JSONObject addressComponent = addressComponents.getJSONObject(j);
                        if (result.has("types")) {
                            JSONArray types = addressComponent.getJSONArray("types");

                            for (int k = 0; k < requiredInformations.length; k++) {

                                for (int l = 0; l < types.length(); l++) {
                                    if (requiredInformations[k].type.value.equals(types.getString(l))) {
                                        if (addressComponent.has("long_name")) {
                                            PostInformation(requiredInformations[k].type,
                                                    addressComponent.getString("long_name"));
                                        } else if (addressComponent.has("short_name")) {
                                            PostInformation(requiredInformations[k].type,
                                                    addressComponent.getString("short_name"));
                                        }
                                    }

                                }

                            }

                        }
                    }
                }
            }
        } catch (Exception ignored) {
            ignored.printStackTrace();
        }
        ANDROID_HTTP_CLIENT.close();
    }

    /**
     * Notify with LocInfoType and its value, and remove from required fields if
     * necessary not to look for and post over and over again
     */
    private void PostInformation(final LocInfoType type, final String value) {
        int requiredIndex = -1;
        for (int i = 0; i < requiredInformations.length; i++) {
            if (requiredInformations[i].type == type) {
                requiredIndex = i;
            }
        }

        if (requiredIndex != -1) {
            if (getActivity() != null && !requiredInformations[requiredIndex].hasSent) {
                requiredInformations[requiredIndex].hasSent = true;
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        onLocationInfoReceived(type, value);
                    }

                });
            }
        }
    }

    /**
     * Check Map availability and glEs version compatibility. If all is well,
     * then calls onMapReady method otherwise calls noMapFound method
     */
    private void CheckMap() {
        if (runningThreadCheckMap)
            return;

        new Thread(new Runnable() {

            @Override
            public void run() {
                runningThreadCheckMap = true;

                // Check GooglePlayService is available or not int
                int playServiceStatus = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getActivity());

                if (playServiceStatus == ConnectionResult.SUCCESS) {
                    // Google Play Services are available
                    if (getVersionFromPackageManager(getActivity()) >= 2) {
                        // OpenGL version is also okay
                        GetMapView();
                    } else {
                        // OpenGL version is not supported
                        if (getActivity() != null) {
                            getActivity().runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    noMapFound(false);
                                }
                            });
                        }
                        runningThreadCheckMap = false;
                    }
                } else if (GooglePlayServicesUtil.isUserRecoverableError(playServiceStatus)) {
                    // Google Play Services are not available on current device
                    new ErrorDialogFragment(playServiceStatus).show(getActivity().getSupportFragmentManager(),
                            TAG_ERROR_DIALOG_FRAGMENT);
                } else {
                    // Any other case that mapView couldn't get found
                    if (getActivity() != null) {
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                noMapFound(false);
                            }
                        });
                    }
                    runningThreadCheckMap = false;
                }

            }

        }).start();

    }

    /**
     * Waits until map is ready
     */
    private void GetMapView() {
        delayHandler.post(new Runnable() {

            @Override
            public void run() {
                mapView = mapFragment.getMap();
                if (mapView == null) {
                    delayHandler.postDelayed(this, CHECK_MY_LOCATION_DELAY);
                } else {
                    if (getActivity() != null) {
                        getActivity().runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                onMapReady();
                            }
                        });
                    }
                    runningThreadCheckMap = false;
                }
            }

        });
    }

    @SuppressLint("ValidFragment")
    private class ErrorDialogFragment extends DialogFragment {

        private final String ARG_STATUS = "status";

        public ErrorDialogFragment(int status) {
            Bundle args = new Bundle();
            args.putInt(ARG_STATUS, status);
            this.setArguments(args);
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            Bundle args = getArguments();

            return GooglePlayServicesUtil.getErrorDialog(args.getInt(ARG_STATUS), getActivity(),
                    REQUEST_CODE_PLAY_SERVICE);
        }

        @Override
        public void onDismiss(DialogInterface dlg) {
            if (getActivity() != null) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        noMapFound(true);
                    }
                });
            }
            runningThreadCheckMap = false;
        }
    }

    // following from
    // https://android.googlesource.com/platform/cts/+/master/tests/tests/graphics/src/android/opengl/cts/OpenGlEsVersionTest.java

    /*
     * Copyright (C) 2010 The Android Open Source Project
     * 
     * 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.
     */

    private int getVersionFromPackageManager(Context context) {
        PackageManager packageManager = context.getPackageManager();
        FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures();
        if (featureInfos != null && featureInfos.length > 0) {
            for (FeatureInfo featureInfo : featureInfos) {
                // Null feature name means this feature is the open
                // GLes version feature.
                if (featureInfo.name == null) {
                    if (featureInfo.reqGlEsVersion != FeatureInfo.GL_ES_VERSION_UNDEFINED) {
                        return getMajorVersion(featureInfo.reqGlEsVersion);
                    } else {
                        // Lack of property means OpenGL ES version 1
                        return 1;
                    }
                }
            }
        }
        return 1;
    }

    /** @see FeatureInfo#getGlEsVersion() */
    private int getMajorVersion(int glEsVersion) {
        return ((glEsVersion & 0xffff0000) >> 16);
    }

}