My Solution for the Hard Challenge


#1

Hi! I hope this will help someone out there, as I managed to solve the Hard Challenge.
I don’t know if it’s the most efficient way, but to me it makes much sense and gets the job done really well!

Here is a link to the source files.
https://www.dropbox.com/s/tu8vzjdcuhg2ydu/CriminalIntentHardDialogChallenge.zip

Here is a link to a gallery showing the flow. (there are buttons at the top to click to the next pictures.)
http://imgur.com/ltrC2iR,DDUou5g,1GXNu4O,UJdx3OJ,Tgey4s0,ot40YFU,gZo9dhN#0

Otherwise, my modifications are as follows.

CrimeFragment.java

package com.justin.criminalintent;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.UUID;

/**
 * Created by Justin on 11/22/13.
 */
public class CrimeFragment extends Fragment {

    public static final String EXTRA_CRIME_ID = "com.justin.criminalintent.crime_id";
    private static final int REQUEST_DATE = 0xff;
    private static final int REQUEST_TIME = 0xfe;
    private static final int REQUEST_CHOICE = 0xfd;

    private Crime mCrime;
    private EditText mTitleField;
    private Button mDateButton;
    private CheckBox mSolvedCheckBox;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UUID crimeId = (UUID)getArguments().getSerializable(EXTRA_CRIME_ID);
        mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_crime, parent, false);

        mTitleField = (EditText)v.findViewById(R.id.crime_title);
        mTitleField.setText(mCrime.getTitle());
        mTitleField.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
                // Left blank
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
                mCrime.setTitle(charSequence.toString());
            }

            @Override
            public void afterTextChanged(Editable editable) {
                // left blank
            }
        });

        mDateButton = (Button)v.findViewById(R.id.crime_date);
        updateDate();
        mDateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
//                FragmentManager fm = getActivity().getSupportFragmentManager();
//                DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
//                dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
//                dialog.show(fm, null);
                editDateTimeDialog();
            }
        });

        mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved);
        mSolvedCheckBox.setChecked(mCrime.isSolved());
        mSolvedCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                mCrime.setSolved(b);
            }
        });
        return v;
    }

    private void editDateTimeDialog() {
        FragmentManager fm = getActivity().getSupportFragmentManager();
        ChoiceDialogFragment dialogFragment = new ChoiceDialogFragment();
        dialogFragment.setTargetFragment(CrimeFragment.this, REQUEST_CHOICE);
        dialogFragment.show(fm, null);
    }

    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 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);
            combineDate(date);
            updateDate();
        }
        if (requestCode == REQUEST_TIME) {
            Date date = (Date)data.getSerializableExtra(TimePickerFragment.EXTRA_TIME);
            combineTime(date);
            updateDate();
        }

        if (requestCode == REQUEST_CHOICE) {
            int choice = data.getIntExtra(ChoiceDialogFragment.EXTRA_CHOICE, 0);
            if (choice == 0) {
                Log.d("choice dialog", "requested choice returned nothing");
                return;
            }
            if (choice == ChoiceDialogFragment.CHOICE_TIME) editTimeDialog();
            else if (choice == ChoiceDialogFragment.CHOICE_DATE) editDateDialog();
        }
    }

    private void editDateDialog() {
        FragmentManager fm = getActivity().getSupportFragmentManager();
        DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
        dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
        dialog.show(fm, null);
    }

    private void editTimeDialog() {
        FragmentManager fm = getActivity().getSupportFragmentManager();
        TimePickerFragment dialog = TimePickerFragment.newInstance(mCrime.getDate());
        dialog.setTargetFragment(CrimeFragment.this, REQUEST_TIME);
        dialog.show(fm, null);
    }


    private void combineTime(Date time) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(mCrime.getDate());
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = cal.get(Calendar.DAY_OF_MONTH);
        cal.setTime(time);
        int hours = cal.get(Calendar.HOUR_OF_DAY);
        int mins = cal.get(Calendar.MINUTE);
        Date finalD = new GregorianCalendar(year, month, day, hours, mins).getTime();
        mCrime.setDate(finalD);
    }

    private void combineDate(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = cal.get(Calendar.DAY_OF_MONTH);
        cal.setTime(mCrime.getDate());
        int hours = cal.get(Calendar.HOUR_OF_DAY);
        int mins = cal.get(Calendar.MINUTE);

        Date finalD = new GregorianCalendar(year, month, day, hours, mins).getTime();
        mCrime.setDate(finalD);
    }

    private void updateDate() {
        mDateButton.setText(mCrime.getDateString());
    }

    public CrimeFragment() {
    }
}

TimePickerFragment.java

[code]package com.justin.criminalintent;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.View;
import android.widget.TimePicker;

import java.util.Calendar;
import java.util.Date;

/**

  • Created by Justin on 11/23/13.
    */
    public class TimePickerFragment extends DialogFragment {
    private Date mDate;
    public static final String EXTRA_TIME = “com.justin.criminalintent.time”;

    public static TimePickerFragment newInstance(Date date) {
    Bundle args = new Bundle();
    args.putSerializable(EXTRA_TIME, date);

     TimePickerFragment fragment = new TimePickerFragment();
     fragment.setArguments(args);
    
     return fragment;
    

    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
    mDate = (Date)getArguments().getSerializable(EXTRA_TIME);

     Calendar cal = Calendar.getInstance();
     cal.setTime(mDate);
     int hours = cal.get(Calendar.HOUR_OF_DAY);
     int mins = cal.get(Calendar.MINUTE);
    
     View v = getActivity().getLayoutInflater().inflate(R.layout.dialog_time, null);
     TimePicker tp = (TimePicker)v.findViewById(R.id.dialog_time_timePicker);
     tp.setCurrentHour(hours);
     tp.setCurrentMinute(mins);
     tp.setOnTimeChangedListener(new TimePicker.OnTimeChangedListener() {
         @Override
         public void onTimeChanged(TimePicker timePicker, int hourOfDay, int minute) {
             Calendar cal = Calendar.getInstance();
             cal.setTime(mDate);
             cal.set(Calendar.HOUR_OF_DAY, hourOfDay);
             cal.set(Calendar.MINUTE, minute);
    
             mDate = cal.getTime();
             getArguments().putSerializable(EXTRA_TIME, mDate);
         }
     });
    
     return new AlertDialog.Builder(getActivity())
             .setView(v)
             .setTitle(R.string.time_picker_title)
             .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                 @Override
                 public void onClick(DialogInterface dialogInterface, int i) {
                     sendResult(Activity.RESULT_OK);
                 }
             }).create();
    

    }

    private void sendResult(int resultCode) {
    if (getTargetFragment() == null) return;
    Intent i = new Intent();
    i.putExtra(EXTRA_TIME, mDate);

     getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, i);
    

    }
    }
    [/code]

ChoiceDialogFragment.java

package com.justin.criminalintent;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;

/**
 * Created by Justin on 11/23/13.
 */
public class ChoiceDialogFragment extends DialogFragment {
    public static final String EXTRA_CHOICE = "com.justin.criminalintent.choice";

    private int mChoice = 0;

    public static final int CHOICE_DATE = 1;
    public static final int CHOICE_TIME = 2;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

        builder.setMessage(R.string.dialog_editDateOrTime)
                .setPositiveButton(R.string.dialog_editDate, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        mChoice = CHOICE_DATE;
                        sendResult(Activity.RESULT_OK);
                    }
                })
                .setNegativeButton(R.string.dialog_editTime, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        mChoice = CHOICE_TIME;
                        sendResult(Activity.RESULT_OK);
                    }
                });
        return builder.create();

    }

    private void sendResult(int resultCode) {
        if (getTargetFragment() == null) return;
        Intent i = new Intent();
        i.putExtra(EXTRA_CHOICE, mChoice);

        getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, i);
    }
}

dialog_time.xml

[code]<?xml version="1.0" encoding="utf-8"?>

[/code]

strings.xml

[code]<?xml version="1.0" encoding="utf-8"?>

<string name="app_name">CriminalIntent</string>
<string name="crime_title_hint">Enter a title for the crime.</string>
<string name="crime_title_label">Title</string>
<string name="crime_details_label">Details</string>
<string name="crime_solved_label">Solved?</string>
<string name="crimes_title">Crimes</string>
<string name="date_picker_title">Date of crime:</string>
<string name="time_picker_title">Time of crimes:</string>
<string name="dialog_editDate">Date</string>
<string name="dialog_editTime">Time</string>
<string name="dialog_editDateOrTime">Do you want to edit the date or time?</string>
[/code]

Crime.java

[code]package com.justin.criminalintent;

import java.text.DateFormat;
import java.util.Date;
import java.util.UUID;

/**

  • Created by Justin on 11/22/13.
    */
    public class Crime {
    private UUID mId;
    private String mTitle;
    private Date mDate;
    private boolean mSolved;

    public Date getDate() {
    return mDate;
    }

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

    public String getDateString() {
    return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(mDate);
    }

    public boolean isSolved() {
    return mSolved;
    }

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

    public UUID getId() {
    return mId;
    }

    public Crime() {
    mId = UUID.randomUUID();
    mDate = new Date();
    }

    public String getTitle() {
    return mTitle;
    }

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

    @Override
    public String toString() {
    return mTitle;
    }
    }
    [/code]


#2

Thanks, for some reason, I thought we should call the timePickerDialog and datePickerDialog through the ChoiceDialog. I was pulling my hair out trying to figure out how to send data back to a fragment, send it to another fragment, then send it back to the original fragment.

It didn’t occur to me to send the intent back to CrimeFragment and then call the respective pickerDialog from there. :laughing:


#3

Thank you for posting your solution. I was really stuck on this, and your code helped me find where I was going wrong. I find the communication between fragments a little tricky but hopefully with practice it will become easier. :slight_smile:


#4

Thank you~ great job!! :smiley:


#5

I just had to come on and say thanks. I had been working on this challenge for a while to no avail, and the idea to introduce an option dialog before choosing either date or time was something I never even considered.

Well done!


#6

Wow very well done provided me an idea just by look at a glance, didn’t realize that i can just call those two dialogs in crimefragment itself! Thank You for your contribution and I hope to see more of your activities in this forum!


#7

I guess it’s the best answer so far, thank you for the hints. Your implementation helped me a lot in a couple of cul-de-sac’s.

Only one thing. I solved all the date format problem in a pretty much simple way, instead of your “combineTime/Date” methods and added methods on the Crime class.

My only method for update the date & time in the button’s text, inside CrimeFragment.java is:

	private void updateDateAndTime() {
		mDateButton.setText(DateFormat.format("EEEE, MMM d, yyyy - kk:mm", mCrime.getDate()).toString());
	}

By using this template date & time are displayed the same way they do in the pics from your gallery.

This method, together with this implementation of the pickers makes the game:

DatePickerFragment.java

package com.battosaihimura.android.criminalintent;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.util.Log;
import android.view.View;
import android.widget.DatePicker;
import android.widget.DatePicker.OnDateChangedListener;

public class DatePickerFragment extends DialogFragment {

	public static final String EXTRA_DATE = "com.battosaihimura.android.criminalintent.date";
	public static final String TAG = "DatePickerFragment";
	
	private Date mDate;
	
	@Override
	@NonNull
	public Dialog onCreateDialog(Bundle savedInstanceState) {
		
		mDate = (Date) getArguments().getSerializable(EXTRA_DATE);
		
		//Create a Calendar and get year, month and day from the mDate String
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(mDate);
		int year = calendar.get(Calendar.YEAR);
		int month = calendar.get(Calendar.MONTH);
		int day = calendar.get(Calendar.DAY_OF_MONTH);
		
		//Creates the Date view based on the dialog_date layout 
		View v = getActivity().getLayoutInflater().inflate(R.layout.dialog_date, null);
		
		//Inflating the datePicker with it's listener (in it anonymous class form) inside the view
		DatePicker datePicker = (DatePicker)v.findViewById(R.id.dialog_date_picker);
		datePicker.init(year, month, day, new OnDateChangedListener() {

			@Override
			public void onDateChanged(DatePicker view, int year, int month, int day) {
				
				//retrieving the original crime time from the mDate value with a calendar 
				Calendar calendar = Calendar.getInstance();
				calendar.setTime(mDate);
				int hour = calendar.get(Calendar.HOUR_OF_DAY);
				int minute = calendar.get(Calendar.MINUTE);
				
				// Translating year, month and day into a Date object using a calendar, time keeps the same
				mDate = new GregorianCalendar(year, month, day, hour, minute).getTime();
				
				//Update argument to preserve selected value on rotation
				getArguments().putSerializable(EXTRA_DATE, mDate);
				Log.d(TAG, "Data set is: " + mDate.toString());
				
			}
			
		});
		
		/*Creates a Dialog with a DatePicker inside and a button to commit changes to the date.
		 * The listener of the button is as usually implemented as an anonymous class but is inside the 
		 * setPositiveButton(String, onClickListener) call as the second parameter.
		 */
		return new AlertDialog.Builder(getActivity())
					.setView(v)
					.setTitle(R.string.date_picker_title)
					.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
						
						@Override
						public void onClick(DialogInterface dialog, int which) {
							sendResult(Activity.RESULT_OK);
							
						}
					})
					.create();
	}

	public static DatePickerFragment newInstance(Date date) {
		Bundle args = new Bundle();
		args.putSerializable(EXTRA_DATE, date);
		
		DatePickerFragment fragment = new DatePickerFragment();
		fragment.setArguments(args);
			
		return fragment;
	}
	
	/*
	 * Method used for packaging the data selected from the user and sending it to the target fragment
	 */
	private void sendResult(int resultCode) {
		if(getTargetFragment() == null) {
			return;
		}
		
		Intent i = new Intent();
		i.putExtra(EXTRA_DATE, mDate);
		
		getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, i);
	}
}

TimePIckerFragment.java

package com.battosaihimura.android.criminalintent;

import java.util.Calendar;
import java.util.Date;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.util.Log;
import android.view.View;
import android.widget.TimePicker;
import android.widget.TimePicker.OnTimeChangedListener;

public class TimePickerFragment extends DialogFragment {
	
	public static final String EXTRA_TIME = "com.battosaihimura.android.criminalintent.time";
	public static final String TAG = "TimePickerFragment";

	private Date mTime;
	
	
	
	@Override
	@NonNull
	public Dialog onCreateDialog(Bundle savedInstanceState) {
		
		mTime = (Date) getArguments().getSerializable(EXTRA_TIME);
		//Creates a Calendar object and extracts hour and minutes from crime's date
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(mTime);
		int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY);
		int minute = calendar.get(Calendar.MINUTE);
		
		//Creates the Time view based on the dialog_time layout
		View v = getActivity().getLayoutInflater().inflate(R.layout.dialog_time, null);
		
		//Inflating the timePicker with it's listener (in it anonymous class form) inside the view
		TimePicker timePicker = (TimePicker) v.findViewById(R.id.dialog_time_picker);
		timePicker.setCurrentHour(hourOfDay);
		timePicker.setCurrentMinute(minute);
		timePicker.setIs24HourView(true);
		timePicker.setOnTimeChangedListener(new OnTimeChangedListener() {

			@Override
			public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
				
				//retrieving the original crime date from the mTime value with a calendar 
				Calendar calendar = Calendar.getInstance();
				calendar.setTime(mTime);
				calendar.set(Calendar.HOUR_OF_DAY, hourOfDay);
				calendar.set(Calendar.MINUTE, minute);
				
				// Translating hourOfDay & minute into a Date object using a calendar, date keeps the same
				mTime = calendar.getTime();
				
				//Update argument to preserve selected value on rotation
				getArguments().putSerializable(EXTRA_TIME, mTime);
				Log.d(TAG, "Time set is: " + mTime.toString());
			}
			
		});
		
		return new AlertDialog.Builder(getActivity())
					.setView(v)
					.setTitle(R.string.time_picker_title)
					.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
						
						@Override
						public void onClick(DialogInterface dialog, int which) {
							sendResult(Activity.RESULT_OK);
							
						}
					})
					.create();
	}



	public static TimePickerFragment newInstance(Date time) {
		Bundle args = new Bundle();
		args.putSerializable(EXTRA_TIME, time);
		
		TimePickerFragment fragment = new TimePickerFragment();
		fragment.setArguments(args);
		
		return fragment;
	}
	
	private void sendResult(int resultCode) {
		if(getTargetFragment() == null) {
			return;
		}
		
		Intent i = new Intent();
		i.putExtra(EXTRA_TIME, mTime);
		
		getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, i);
	}
}

Of course, all the interesting part is inside the OnDateChangedListener and OnTimeChangedListener implementations.

Since most of the code was the same of the book, there are two different operating ways, with a Calendar object and with the GregorianCalendar object. But I find the fist one is way much better.

Hope this was of some help, didn’t want to seem cocky.

Thanks again for sharing your code, without it I’d still be trying to find a way to make things work.


#8

When I update my code I run the project and I get to the Choice Dialog Fragment and after I choose to modify either the date or time it returns me to the crime I started on. I think it may be that the choice dialog fragment is returning null which returns me to the current crime. Do I need to modify my AndroidManifest.xml?


#9

Can you explain to me about the use of 0xff, 0xfe and 0xfd for the int constants? Is there any reason to not do this with, for example, 1, 2 and 3?


#10

I decided to go one step farther on this one.
I implemented CompareTo on the Crime class and made the corresponding method compare based on dates.
Then I ran Collections.sort(mCrimes); in the onResume method in CrimeListFragment, just before notifyDataSetChanged.
Automatically sorting crimes by date seemed a pretty good default. It hasn’t caused any errors so far on my testing.