Major Bug no errors - adding a crime changes first crime


#1

Hi, all.

I have a strange bug going on. When I click the add crime button in the actionbar, for some reason it keeps displaying the first crime in the list. When I change the crime title in the edittext and click back, a new crime is added to the bottom of the list. However, the top crime in the list always changes to what I put in the EditText for the newly added crime. On top of that, the new crime added to the bottom of the list no title. If I didn’t explain it well enough here is what happens: (with pictures! yeeaay! lol)

default empty list view on app startup:

after clicking addCrime actionbar button:

back button to go back to list:

Looks great up till here, then I click the addCrime action bar button again to see this:

I change the crime title to “crime 2” then hit the back button and then this is what the list looks like:

just for kicks and further investigation i clicked the addCrime actionbar button a third time and see this:

change the title to crime 3, hit the back button, and then I see the even more messed up list:

I know it is probably something simple in the code that I am missing or misstyped from the book. I think the problem may be related to the CrimePagerActivity always displaying the first crime in the list because of the pageSelected listener never being called after adding a crime. Is anyone else having this problem?

Any ideas on how to fix this?


#2

No one knows the answer to the above problem? really?

I can summarize the main problem better:
The ViewPager always shows the first crime when I click the addCrime actionbar button. So the main problem here is in the way the ViewPager gets the serializable extra crimeId (if it is even getting one at all).

Why is the ViewPager always displaying the first crime in the list?

Everything else works perfect. I can swipe from crime to crime using the viewpager. When I click a crime in the list, the ViewPager displays the correct crime fragment. Also, deleting multiple crimes from the list works perfectly. It always deletes the correct crimes that were selected in the multi-modal selection thing.

Any ideas where in the code the ViewPager would not be getting updated with the position of the new crime that was just added before the CrimePagerActivity was started?


#3

Sorry for the delay in getting to your question. Could you show us the code for CrimeFragment and CrimePagerActivity?


#4

CrimeFragment.java:

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

import java.util.Date;
import java.util.UUID;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;

public class CrimeFragment extends Fragment {
// Index key/value pair for savedInstanceState bundle
public static final String EXTRA_CRIME_ID =
“com.bignerdranch.android.criminalintent.crime_id”;

// DatePickerFragment's tag:
private static final String DIALOG_DATE = "date";

// request code for target fragment *used to pass date from datePicker to crimefragment
private static final int REQUEST_DATE = 0;

private Crime mCrime;
private EditText mTitleField;
private Button mDateButton;
private CheckBox mSolvedCheckBox;
//private SimpleDateFormat mDate;
//private String mDatePattern;
//private String mFormattedDate;

// Method to create a new instance of crimeFragment with the passed in crimeId 
//       then bundle that crimeId to that crime fragment object.
public static CrimeFragment newInstance(UUID crimeId) {
	Bundle args = new Bundle();
	args.putSerializable(EXTRA_CRIME_ID, crimeId);
	
	CrimeFragment fragment = new CrimeFragment();
	fragment.setArguments(args);
	
	return fragment;
}

@Override
public void onPause() {
	super.onPause();
	// save crimes array to JSON file if app is paused
	CrimeLab.get(getActivity()).saveCrimes();
}

@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	//UUID crimeId = (UUID)getActivity().getIntent()
		//	.getSerializableExtra(EXTRA_CRIME_ID);
	UUID crimeId = (UUID)getArguments().getSerializable(EXTRA_CRIME_ID);
	mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);
	
	// Turn on options menu handling
	setHasOptionsMenu(true);
}
		
@Override
public boolean onOptionsItemSelected(MenuItem item) {
	switch (item.getItemId()) {
		case android.R.id.home:
			// if there is a valid parent activity named in the manifest, navigate up to it
			if (NavUtils.getParentActivityName(getActivity()) != null) {
				NavUtils.navigateUpFromSameTask(getActivity());
			}
			return true;
		default:
			return super.onOptionsItemSelected(item);
	}
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
	if (resultCode != Activity.RESULT_OK) return;
	if (requestCode == REQUEST_DATE) {
		Date date = (Date)data
				.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
		mCrime.setDate(date);
		//mDateButton.setText(mCrime.getDate().toString());
		updateDate();
	}
}

public void updateDate() {
	mDateButton.setText(mCrime.getDate().toString());
}

@TargetApi(11)
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
	// Explicitly inflate the fragment layout as a view object
	View v = inflater.inflate(R.layout.fragment_crime, parent, false);
	
	// Turn on the up button in the action bar
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
		if (NavUtils.getParentActivityName(getActivity()) != null) {
			getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
		}
	}
	
	// Use this view object to assign a reference of the EditText to a variable
	mTitleField = (EditText)v.findViewById(R.id.crime_title);
	if (mCrime.getTitle() != null) {
		mTitleField.setText(mCrime.getTitle());
	}
	mTitleField.addTextChangedListener(new TextWatcher() {
		public void onTextChanged(CharSequence c, int start, int before, int count) {
			mCrime.setTitle(c.toString());
		}
		
		public void beforeTextChanged(CharSequence c, int start, int count, int after) {
			// This space intentionally left blank
		}
		
		public void afterTextChanged(Editable c) {
			// This one too
		}
	});
	

	
	// Challenge 1 chapter 8 - get and display the current date in the format like Saturday, Jan 9, 2013
	//mDatePattern = "EEEE, MMM dd, yyyy";
	//mDate = new SimpleDateFormat(mDatePattern);
	//mFormattedDate = mDate.format(new Date());
					
	mDateButton = (Button)v.findViewById(R.id.crime_date);
	//mDateButton.setText(mFormattedDate);
	//mDateButton.setEnabled(false);
	updateDate();
	
	// click listener for date button that starts the alert dialog / date picker
	mDateButton.setOnClickListener(new View.OnClickListener() {
		
		@Override
		public void onClick(View v) {
			FragmentManager fm = getActivity()
					.getSupportFragmentManager();
			//DatePickerFragment dialog = new DatePickerFragment();
			DatePickerFragment dialog = DatePickerFragment
					.newInstance(mCrime.getDate());
			dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
			dialog.show(fm,  DIALOG_DATE);				
		}
	});
	
	mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved);
	mSolvedCheckBox.setChecked(mCrime.isSolved());
	mSolvedCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
		public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
			//Set the crime's solved property
			mCrime.setSolved(isChecked);
		}
	});
	
	return v;
}

}
[/code]

CrimePagerActivity.java:

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

import java.util.ArrayList;
import java.util.UUID;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;

public class CrimePagerActivity extends FragmentActivity {
private ViewPager mViewPager;
private ArrayList mCrimes;

private static final String TAG2 = "Crime_ID_Check";

@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	
	mViewPager = new ViewPager(this);
	mViewPager.setId(R.id.viewPager); // ID is setup in res/values/ids.xml
	setContentView(mViewPager);
	
	mCrimes = CrimeLab.get(this).getCrimes();
	
	// Setup the adapter for this viewpager using FragmentStatePagerAdapter
	//		this is a simplified adapter for fragments that only uses two methods: getCount() and getItem(int)
	FragmentManager fm = getSupportFragmentManager();
	mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) {
		@Override
		public int getCount() {
			return mCrimes.size();
		}
		
		@Override
		public Fragment getItem(int pos) {
			Crime crime = mCrimes.get(pos);
			return CrimeFragment.newInstance(crime.getId());
		}
	});
	
	// Add OnPageListener to change the activity's title based on the current crime being viewed
	mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
		
		@Override
		public void onPageScrollStateChanged(int state) {	}
		
		@Override
		public void onPageScrolled(int pos, float posOffset, int posOffsetPixels) {	}
		
		@Override
		public void onPageSelected(int pos) {
			Crime crime = mCrimes.get(pos);
			if (crime.getTitle() != null) {
				setTitle(crime.getTitle());
			}				
		}						
	});
	
	// Set Initial Pager Item
	UUID crimeId = (UUID)getIntent().getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID);
	for (int i = 0; i < mCrimes.size(); i++) {
		if (mCrimes.get(i).getId().equals(crimeId)) {
			mViewPager.setCurrentItem(i);
			
			if (i == 0) {   // ViewPager starts with item 0, so force the title update here since onPageSelected is not called
				setTitle(mCrimes.get(i).getTitle());
			}
			break;
		} // end if
	} // end for
	
	
	
} // end onCreate

} // end class CrimePagerActivity
[/code]

Hope that helps. I know it’s something simple, but I just don’t understand how the ViewPager starts up. I know it’s supposed to search the crimes array for the crime with the same crimeId that was passed in as an extra. The part I don’t understand is if it does this search every time it is called after clicking the new crime actionbar button. I am still very confused about how we are passing argument bundles in between activities and fragments. I wish Google would make fragments the standard option for Android apps as well as reduce the significant amount of code needed for them to function properly.


#5

Hmm. The plot grows thicker.

It looks like you’re passing in the Crime id correctly, and looking for the crime correctly. But you’re still not getting the right Crime.

Perhaps the problem is in CrimeLab?


#6

I am not really sure. I’ve been searching for the problem for 2 weeks now. I don’t want to move on with chapter 19 till I resolve this problem. The more code that’s added the harder it becomes to fix issues like this.

The methods in CrimeLab seem to work fine for every other part of the app. so I am not sure what could be wrong in there.

Here is my CrimeLab.java:

package com.bignerdranch.android.criminalintent;

import java.util.ArrayList;
import java.util.UUID;
import android.content.Context;
import android.util.Log;

public class CrimeLab {
	private static final String TAG = "CrimeLab";
	private static final String FILENAME = "crimes.json";	
	
	private ArrayList<Crime> mCrimes;
	private CriminalIntentJSONSerializer mSerializer;
	
	private static CrimeLab sCrimeLab;
	private Context mAppContext;
	
	// Private constructor for this Singleton class
	private CrimeLab(Context appContext) {
		mAppContext = appContext;
		// commented out to load crimes from JSON file
		//mCrimes = new ArrayList<Crime>();
		mSerializer = new CriminalIntentJSONSerializer(appContext, FILENAME);
		
		try {
			mCrimes = mSerializer.loadCrimes();
		} catch (Exception e) {
			// if no crime file exists, create a new mCrimes ArrayList
			mCrimes = new ArrayList<Crime>();
			Log.e(TAG, "Error loading crimes: ", e);
		}
	}
	
	// Check for an existing instance of a CrimeLab object. Create one if it doesn't exist already.
	public static CrimeLab get(Context c) {
		if (sCrimeLab == null) {
			sCrimeLab = new CrimeLab(c.getApplicationContext());
		}
		return sCrimeLab;
	}
	
	public void deleteCrime(Crime c) {
		mCrimes.remove(c);
	}
	
	public void addCrime(Crime c) {
		mCrimes.add(c);
	}
	
	public ArrayList<Crime> getCrimes() {
		return mCrimes;
	}
	
	public Crime getCrime(UUID id) {
		for (Crime c : mCrimes) {
			if (c.getId().equals(id)) {
				return c;
			}
		}
		return null;
	}
	
	public boolean saveCrimes() {
		try {
			mSerializer.saveCrimes(mCrimes);
			Log.d(TAG, "crimes saved to file");
			return true;
		} catch (Exception e) {
			Log.e(TAG, "Error saving crimes: ", e);
			return false;
		}
	} // end saveCrimes method
	
} // end CLASS CrimeLab

#7

I’m still at a loss. What about Crime?


#8

Here is Crime.java:

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

import java.util.Date;
import java.util.UUID;

import org.json.JSONException;
import org.json.JSONObject;

public class Crime {

private static final String JSON_ID = "id";
private static final String JSON_TITLE = "title";
private static final String JSON_SOLVED = "solved";
private static final String JSON_DATE = "date";

private UUID mId;
private String mTitle;
private Date mDate;
private boolean mSolved;
//private String mFormattedDate;

public Crime() {
	// Generate unique identifier
	mId = UUID.randomUUID();
	mDate = new Date();
	mTitle = null;
}

// add a constructor that loads the JSON file data
public Crime(JSONObject json) throws JSONException {
	mId = UUID.fromString(json.getString(JSON_ID));
	mTitle = json.getString(JSON_TITLE);
	mSolved = json.getBoolean(JSON_SOLVED);
	mDate = new Date(json.getLong(JSON_DATE));
}

public JSONObject toJSON() throws JSONException {
	JSONObject json = new JSONObject();
	json.put(JSON_ID,  mId.toString());
	json.put(JSON_TITLE, mTitle);
	json.put(JSON_SOLVED, mSolved);
	json.put(JSON_DATE, mDate.getTime());
	return json;
}

public String getTitle() {		
	return mTitle;		
}

public void setTitle(String title) {
	mTitle = title;
}

public UUID getId() {
	return mId;
}

public Date getDate() {
	//mDate.applyPattern("EEEE, MMM dd, yyyy");
	//mDate = new SimpleDateFormat("EEEE, MMM dd, yyyy");
	//mFormattedDate = mDate.format(new Date());
	return mDate;
}

public void setDate(Date date) {
	mDate = date;
}

public boolean isSolved() {
	return mSolved;
}

public void setSolved(boolean solved) {
	mSolved = solved;
}

@Override
public String toString() {
	return mTitle;
}

}
[/code]


#9

So no one else has this problem? Can someone please review my code above and compare it to what you have if your app works correctly?

I’m off to compare the source code with what I have again. Hopefully now that I’ve taken some time off the problem will jump out at my now fresh eyes. :smiley:


#10

OMFG! FINALLY!!!

I found the bug after comparing my code with the downloaded source code. Everything is working fine now. Can you believe the culprit was 1 missing line of code?

For anyone else with the issue of always changing the first item in a list whenever you try to add a new one:

MAKE SURE YOU ADD THE EXTRA! (in CrimeListFragment.java within the onOptionsItemSelected() method for your add new item switch case)
I was missing:
i.putExtra(CrimeFragment.EXTRA_CRIME_ID, crime.getId());

case R.id.menu_item_new_crime:
				Crime crime = new Crime();
				CrimeLab.get(getActivity()).addCrime(crime);
				Intent i = new Intent(getActivity(), CrimePagerActivity.class); // SOURCE uses CrimeActivity.class 
				//Intent i = new Intent(getActivity(), CrimeActivity.class); // force closes app when tested for some reason
				i.putExtra(CrimeFragment.EXTRA_CRIME_ID, crime.getId()); // ADDED: was not adding the new crime to the extra bundle
				startActivityForResult(i, 0);
				return true;

this one missing line had me stumped for 2 weeks.

While comparing my code to the source code I noticed a couple other things missing too :frowning:. Apparently I wasn’t saving the crimes to JSON format after adding or deleting a crime. woops.

And maybe this should be another topic, but the source code uses the CrimeActivity.class call when creating the new Intent within onOptionsItemSelected. My code uses CrimePagerActivity.class. When I try to use CrimeActivity.class with my code, the app crashes at startup. The thing that gets me scratching my head is that the source code doesn’t crash. I did add the CrimeActivity.java file back into the project (the book tells us to get rid of that file at some point, but the source code still has it). Is CrimePagerActivity considered the parent activity? I am very confused here.