Challenge Solution


#1

Since this seems to be the only topic without a suggested solution I guess it was either extremely easy or people don’t reach the last page of this book.

Anyway, here is my solution:

First thing suggested was to use an anonymous LocationReceiver class in RunMapFragment so I implemented this and Overrode the method onLocationReceived.

We need to reload the information gathered fron the SQL tables and since we are now using loaders, we need use the restartLoader method to do this (method mentioned in page 562).

I created a method to do this:

private void restartLoader(){ Bundle args = getArguments(); if (args != null) { long runId = args.getLong(ARG_RUN_ID, -1); if (runId != -1) { LoaderManager lm = getLoaderManager(); lm.restartLoader(LOAD_LOCATIONS, args, this); //method used to force loader to restart } } }
Then called this method in onLocationReceived after using mGoogleMap.clear to remove all overlays from map. Now, although this did work, it was flicking and reloading everytime a new location was received and made it quite annoying.

To resolve this little issue I used a Handler to impose a 5 second delay so that the map would only update every 5 seconds. To ensure that the code only runs once every 5 seconds, I used boolean checks and also checked that the fragment is visible to user in case the activity is closed as it could cause a force close if not used.

Don’t forget to initialize: private boolean mDelayed;

anonymous LocationReceiver class in RunMapFragment:

private LocationReceiver mLocationReceiver = new LocationReceiver(){ @Override protected void onLocationReceived(Context context, Location loc) { if(!mDelayed) { //checks if it is delayed and in countdown mDelayed = true; final Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { if(!isVisible()) //Check fragment is visible to user, if we didn't use this, the activity would return; // crash when pressing back as the code below would still run after the delay mGoogleMap.clear(); //removes overlays of map restartLoader(); mDelayed = false; } }, 5000); //number of seconds to delay } } };

And, we need to override the onStart method to manually start the LocationReceiver and also stop it with the onStop method:

[code]@Override
public void onStart() {
super.onStart();
getActivity().registerReceiver(mLocationReceiver,
new IntentFilter(RunManager.ACTION_LOCATION));
}

@Override
public void onStop() {
    getActivity().unregisterReceiver(mLocationReceiver);
    super.onStop();
}[/code]

And that is it! Last challenge complete.

The book was GREAT, and can’t wait for the 2nd edition!
I know that Brian and Bill have been understandably inactive given the age of the book but just in case.
If possible as a request, I think a topic on JSON parsing text and images from web and saving these in SQLite DB would be a great topic to learn.

Lastly, could someone explain the little problem I encounter here, viewtopic.php?f=431&t=9647


#2

Thanks for your solution but I copied and pasted the code in RunMapFragment and I dont see an drawing happening while map is opened.
If I close the map and reopen it, then lines are drawn but again, there is no real life (or every 5 sec) drawing while map is opened.


#3

[quote=“dbnex14”]Thanks for your solution but I copied and pasted the code in RunMapManager and I dont see an drawing happening while map is opened.
If I close the map and reopen it, then lines are drawn but again, there is no real life (or every 5 sec) drawing while map is opened.[/quote]

All the code I posted has to go in RunMapFragment as mentioned in post, not RunMapManager.


#4

Hi mrubix, sorry, I meant I posted the code in RunMapFragment but it is not doing anything.
I put break point in onLocationReceived and in restartLoader and they are never called
Below is my code, as you can see your code is at the very bottin in RunMapFragment but it is not working.
The only way I see map being updated is when I go back to RunFragment, then click on Map button to open map again but the real-time drawing is not happening.
Any idea if I am missing something?
Much appreciated,

package com.learning.mypack.runtracker;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Color;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

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.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

import java.text.SimpleDateFormat;
import java.util.Date;


/**
 * Created by ...
 */
public class RunMapFragment extends SupportMapFragment implements LoaderManager.LoaderCallbacks<Cursor> {
    private static final String ARG_RUN_ID = "RUN_ID";
    private static final int LOAD_LOCATIONS = 0;

    private GoogleMap mGoogleMap;
    private RunDatabaseHelper.LocationCursor mLocationCursor;
    private boolean mDelayed;

    public static RunMapFragment newInstance(long runId){
        Bundle args = new Bundle();
        args.putLong(ARG_RUN_ID, runId);
        RunMapFragment rf = new RunMapFragment();
        rf.setArguments(args);

        return rf;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState){
        View v = super.onCreateView(inflater, parent, savedInstanceState);

        //Stash a reference to the GoogleMap
        mGoogleMap = getMap();

        //Show users location
        mGoogleMap.setMyLocationEnabled(true);
        //mGoogleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); //to change map type

        return v;
    }

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);

        //Check for a run Id as an argument, and find the run
        Bundle args = getArguments();
        if (args != null){
            long runId = args.getLong(ARG_RUN_ID, -1);
            if (runId != -1){
                LoaderManager lm = getLoaderManager();
                lm.initLoader(LOAD_LOCATIONS, args, this);
            }
        }
    }

    private void updateUI(){
        if (mGoogleMap == null || mLocationCursor == null)
            return;

        //Set up an overlay on the map for this run's locations
        //Creaate a polyline with all of the points
        PolylineOptions line = new PolylineOptions();
        //Also create a LotLngBounds so you can zoom to fit
        LatLngBounds.Builder latLngBuilder = new LatLngBounds.Builder();
        //Iterate over the locations
        mLocationCursor.moveToFirst();
        while(!mLocationCursor.isAfterLast()){
            Location loc = mLocationCursor.getLocation();
            LatLng latLng = new LatLng(loc.getLatitude(), loc.getLongitude());

            SimpleDateFormat df = new SimpleDateFormat("MMM d, yyyy hh:mm");

            Resources r = getResources();
            //If this is first location, add a marker for it
            if(mLocationCursor.isFirst()){
                String startDate = df.format(new Date(loc.getTime())).toString();
                MarkerOptions startMarkerOptions = new MarkerOptions()
                        .position(latLng)
                        .title(r.getString(R.string.run_start))
                        .snippet(startDate)
                        .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN));
                mGoogleMap.addMarker(startMarkerOptions);
            }else if (mLocationCursor.isLast()){
                //If this is the last location, and not also the first, add a marker
                String endDate = df.format(new Date(loc.getTime())).toString();
                MarkerOptions finishMarkerOptions = new MarkerOptions()
                        .position(latLng)
                        .title(r.getString(R.string.run_finish))
                        .snippet(endDate);
                mGoogleMap.addMarker(finishMarkerOptions);
            }

            line.add(latLng);
            line.width(5);
            line.color(Color.RED);
            latLngBuilder.include(latLng);
            mLocationCursor.moveToNext();
        }
        //Add the polyline to the map
        mGoogleMap.addPolyline(line);
        //Make the map zoom to show the track, with some padding
        //Use the size of the current display in pixels as a bounding box
        Display display = getActivity().getWindowManager().getDefaultDisplay();
        //Construct a movement instruction for the map camera
        LatLngBounds latLngBounds = latLngBuilder.build();
        CameraUpdate movement = CameraUpdateFactory.newLatLngBounds(latLngBounds, display.getWidth(), display.getHeight(), 15);
        mGoogleMap.moveCamera(movement);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args){
        long runId = args.getLong(ARG_RUN_ID, -1);
        return new LocationListCursorLoader(getActivity(), runId);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor){
        mLocationCursor = (RunDatabaseHelper.LocationCursor)cursor;
        updateUI();
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader){
        //Stop using the data
        mLocationCursor.close();
        mLocationCursor = null;
    }

    //
    // Real time drawing line on map while map is visible
    //
    //annonymous LocationReceiver class to handle drawing of lines
    private BroadcastReceiver mLocationReceiver = new LocationReceiver(){
        @Override
        protected void onLocationReceived(Context context, Location loc) {
            if(!mDelayed) {               //checks if it is delayed and in countdown
                mDelayed = true;
                final Handler handler = new Handler();
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if(!isVisible())   //Check fragment is visible to user, if we didn't use this, the activity would
                            return;        // crash when pressing back as the code below would still run after the delay
                        mGoogleMap.clear();                         //removes overlays of map
                        restartLoader();
                        mDelayed = false;
                    }
                }, 5000);                     //number of seconds to delay
            }
        }
    };
    
    private void restartLoader(){
        Bundle args = getArguments();
        if (args != null) {
            long runId = args.getLong(ARG_RUN_ID, -1);
            if (runId != -1) {
                LoaderManager lm = getLoaderManager();
                lm.restartLoader(LOAD_LOCATIONS, args, this); //method used to force loader to restart
            }
        }
    }
}

#5

I have had a quick check and seen that you have used

       //annonymous LocationReceiver class to handle drawing of lines
       private [b]BroadcastReceiver[/b] mLocationReceiver = new LocationReceiver(){...

I used, private LocationReceiver mLocationReceiver = new LocationReceiver(){…

I think that could be your mistake, I didn’t change or add anything else that was not in the book or that I didn’t mention.


#6

[quote=“mrubix”]I have had a quick check and seen that you have used

       //annonymous LocationReceiver class to handle drawing of lines
       private [b]BroadcastReceiver[/b] mLocationReceiver = new LocationReceiver(){...

I used, private LocationReceiver mLocationReceiver = new LocationReceiver(){…

I think that could be your mistake, I didn’t change or add anything else that was not in the book or that I didn’t mention.[/quote]

I was just trying something else, changed it back to LocationReceiver but still same issue, no change, map is still not updating at all.

    //annonymous LocationReceiver class to handle drawing of lines
    private LocationReceiver  mLocationReceiver = new LocationReceiver(){
        @Override
        protected void onLocationReceived(Context context, Location loc) {
            if(!mDelayed) {               //checks if it is delayed and in countdown
                mDelayed = true;
                final Handler handler = new Handler();
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if(!isVisible())   //Check fragment is visible to user, if we didn't use this, the activity would
                            return;        // crash when pressing back as the code below would still run after the delay
                        mGoogleMap.clear();                         //removes overlays of map
                        restartLoader();
                        mDelayed = false;
                    }
                }, 500);                     //number of seconds to delay
            }
        }
    };

    private void restartLoader(){
        Bundle args = getArguments();
        if (args != null) {
            long runId = args.getLong(ARG_RUN_ID, -1);
            if (runId != -1) {
                LoaderManager lm = getLoaderManager();
                lm.restartLoader(LOAD_LOCATIONS, args, this); //method used to force loader to restart
            }
        }
    }

#7

OOOOHH CRAAAAP!

I completely forgot to add a crucial part which is to override the onStart and onStop in RunMapFragment.

@Override public void onStart() { super.onStart(); getActivity().registerReceiver(mLocationReceiver, new IntentFilter(RunManager.ACTION_LOCATION)); }

@Override public void onStop() { getActivity().unregisterReceiver(mLocationReceiver); super.onStop(); }

Sorry about that, completely missed it! Will add it on the original post for anyone who looks it up in the future.


#8

Thanks mrubix,
by the way, I compared my code in all classes with the code in the book. So, haven’t added anything new other than your code in RunMapFragment.
Unfortunately, I don’t see it drawing on the map every 5 sec as I would expect to happen. The code does not seem to be called at all for some reason.
I will do additional comparison with book code and see if I get it resolved but as explained, all of this is working as per book except your code.

The onStart and onStop overrides you provided above are already in my code. I think that was book code, p.535 :slight_smile:

Thanks for your help.


#9

From the code snippet you provided which contains the code for RunMapFragment you provided, you don’t have onStart() and onStop() methods. Also, the fact that you mentioned LocationReceiver was never called is a good indication that these weren’t there.

In any case, you might as well compare the whole of my RunMapFragment since it seems you really want to get it to work.

[code]package com.bignerdranch.android.runtracker;

import android.content.Context;

import android.content.IntentFilter;
import android.content.res.Resources;
import android.database.Cursor;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

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;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

import java.util.Date;

public class RunMapFragment extends SupportMapFragment implements LoaderManager.LoaderCallbacks {
private static final String ARG_RUN_ID = “RUN_ID”;
private static final int LOAD_LOCATIONS = 0;

private GoogleMap mGoogleMap;
private RunDatabaseHelper.LocationCursor mLocationCursor;
private boolean mDelayed;


private LocationReceiver mLocationReceiver = new LocationReceiver(){
    @Override
    protected void onLocationReceived(Context context, Location loc) {
        if(!mDelayed) {               //checks if it is delayed and in countdown
            mDelayed = true;
            final Handler handler = new Handler();
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if(!isVisible())   //Check fragment is visible to user, if we didn't use this, the activity would
                        return;        // crash when pressing back as the code below would still run after the delay
                    mGoogleMap.clear();                         //removes overlays of map
                    restartLoader();
                    mDelayed = false;
                }
            }, 5000);
        }
    }
};

private void restartLoader(){
    Bundle args = getArguments();
    if (args != null) {
        long runId = args.getLong(ARG_RUN_ID, -1);
        if (runId != -1) {
            LoaderManager lm = getLoaderManager();
            lm.restartLoader(LOAD_LOCATIONS, args, this); //method used to force loader to restart
        }
    }
}

@Override
public void onStart() {
    super.onStart();
    getActivity().registerReceiver(mLocationReceiver,
            new IntentFilter(RunManager.ACTION_LOCATION));
}

@Override
public void onStop() {
    getActivity().unregisterReceiver(mLocationReceiver);
    super.onStop();
}

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Check for a Run ID as an argument, and find the run
    Bundle args = getArguments();
    if (args != null) {
        long runId = args.getLong(ARG_RUN_ID, -1);
        if (runId != -1) {
            LoaderManager lm = getLoaderManager();
            lm.initLoader(LOAD_LOCATIONS, args, this);
        }
    }
}

public static RunMapFragment newInstance(long runId) {
    Bundle args = new Bundle();
    args.putLong(ARG_RUN_ID, runId);
    RunMapFragment rf = new RunMapFragment();
    rf.setArguments(args);
    return rf;
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
                         Bundle savedInstanceState) {
    View v = super.onCreateView(inflater, parent, savedInstanceState);



    // Stash a reference to the GoogleMap
    mGoogleMap = getMap();
    // Show the user's location
    mGoogleMap.setMyLocationEnabled(true);

    return v;
}

private void updateUI() {
    if (mGoogleMap == null || mLocationCursor == null)
        return;
    // Set up an overlay on the map for this run's locations
    // Create a polyline with all of the points
    PolylineOptions line = new PolylineOptions();
    // Also create a LatLngBounds so you can zoom to fit
    LatLngBounds.Builder latLngBuilder = new LatLngBounds.Builder();
    // Iterate over the locations
    mLocationCursor.moveToFirst();
    while (!mLocationCursor.isAfterLast()) {
        Location loc = mLocationCursor.getLocation();
        LatLng latLng = new LatLng(loc.getLatitude(), loc.getLongitude());
        Resources r = getResources();
        // If this is the first location, add a marker for it
        if (mLocationCursor.isFirst()) {
            String startDate = new Date(loc.getTime()).toString();
            MarkerOptions startMarkerOptions = new MarkerOptions()
                    .position(latLng)
                    .title(r.getString(R.string.run_start))
                    .snippet(r.getString(R.string.run_started_at_format, startDate));
            mGoogleMap.addMarker(startMarkerOptions);
        } else if (mLocationCursor.isLast()) {
            // If this is the last location, and not also the first, add a marker
            String endDate = new Date(loc.getTime()).toString();
            MarkerOptions finishMarkerOptions = new MarkerOptions()
                    .position(latLng)
                    .title(r.getString(R.string.run_finish))
                    .snippet(r.getString(R.string.run_finished_at_format, endDate));
            mGoogleMap.addMarker(finishMarkerOptions);
        }

        line.add(latLng);
        latLngBuilder.include(latLng);
        mLocationCursor.moveToNext();
    }
    // Add the polyline to the map
    mGoogleMap.addPolyline(line);
    // Make the map zoom to show the track, with some padding
    // Use the size of the current display in pixels as a bounding box
    Display display = getActivity().getWindowManager().getDefaultDisplay();
    // Construct a movement instruction for the map camera
    LatLngBounds latLngBounds = latLngBuilder.build();
    CameraUpdate movement = CameraUpdateFactory.newLatLngBounds(latLngBounds,
            display.getWidth(), display.getHeight(), 15);
    mGoogleMap.moveCamera(movement);
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    long runId = args.getLong(ARG_RUN_ID, -1);
    return new LocationListCursorLoader(getActivity(), runId);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    mLocationCursor = (RunDatabaseHelper.LocationCursor)cursor;
    updateUI();
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    // Stop using the data
    mLocationCursor.close();
    mLocationCursor = null;
}

}
[/code]


#10

Thanks mrubix, … you were right, I was missing onStart and onStop in RunMapFragment.
I had it in RunFragment as per book but obviously it is needed in RunMapFragment as well.
Thank you so much for your help, now it is working :slight_smile:


#11

The reason for the need for a delay is because the insert into the database is necessary before you redraw the map (restart the loader).
I achieved this by sending an ordered broadcast and setting priority on the 3 intent filters, the two dynamic receivers have their intent filters set when they are registered in onStart with zero priority and the third is of course in the manifest with priority 999.

cheers on finishing the book
~