TimePicker - Rotation - pre-Honeycomb bug?


#1

Hi,

There appears to be an issue with android.widget.TimePicker on pre-Honeycomb devices (API 8 and 10) when rotating the device.

On android devices, API 15 and above, rotating the orientation works/resumes as expected.

On android devices, API 8 and 10, the TimePicker widget attempts to draw the hour field using the minutes field value. If the minute value exceeds the maximum hour in day range (e.g. 25 minutes), then a blank hour field is displayed. I am using the emulator to perform these tests as I only have an API 17 level hardware device.

I have verified the widget has restored the correct hour value (logging the result of TimePicker.getCurrentHour()) on device rotation. The issue seems to be with how it is trying to display it. I have tried setting the widgit’s hour value to zero and back again to see if I could ‘trick’ it into refreshing the display correctly. I have tried this in both onCreateDialog() and in an onResume() override to no effect.

While I understand this issue may be an android backwards compatibility issue, verifying our apps work on devices down to API level 8 is recommended by the book, to ensure a broad market.

Can you provide advice on how such an issue would be overcome in the ‘real’ world?

Here is my implementation of the TimePickerFragment

package com.bignerdranch.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.v4.app.DialogFragment;
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.bignerdranch.android.criminalintent.time";
	public static final String TAG = "TimePickerFragment";
	
	private Date mTime;
	
	public static TimePickerFragment newInstance(Date date) {
		Bundle args = new Bundle();
		args.putSerializable(EXTRA_TIME, date);
		
		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);
	}
	
	@Override
	public Dialog onCreateDialog(Bundle savedInstanceState) {
		mTime = (Date)getArguments().getSerializable(EXTRA_TIME);
		
		// Create a Calendar object to get the year, month and day
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(mTime);
		int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY);
		int minute = calendar.get(Calendar.MINUTE);
				
		View v = getActivity().getLayoutInflater().inflate(R.layout.dialog_time, null);
		
		TimePicker timePicker = (TimePicker)v.findViewById(R.id.dialog_date_timePicker);
		timePicker.setIs24HourView(true);
		timePicker.setCurrentHour(hourOfDay);
		timePicker.setCurrentMinute(minute);
		timePicker.setOnTimeChangedListener(new OnTimeChangedListener() {
			@Override
			public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
				// Get currently stored DateTime
				Calendar calendar = Calendar.getInstance();
				calendar.setTime(mTime);

				// Update Time while preserving Date
				calendar.set(Calendar.HOUR_OF_DAY, hourOfDay);
				calendar.set(Calendar.MINUTE, minute);
				calendar.set(Calendar.SECOND, 0);

				// Update argument to preserve selected value in rotation
				mTime = calendar.getTime();
				getArguments().putSerializable(EXTRA_TIME, mTime);
			}
		});
			
		return new AlertDialog.Builder(getActivity())
			.setTitle(R.string.time_picker_title)
			.setView(v)
			.setPositiveButton(android.R.string.ok, 
					new DialogInterface.OnClickListener() {
						@Override
						public void onClick(DialogInterface dialog, int which) {
							sendResult(Activity.RESULT_OK);
						}
					})
			.create();
	}

}

Here is the code for CrimeFragment:

package com.bignerdranch.android.criminalintent;

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

import android.annotation.SuppressLint;
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.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.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;

public class CrimeFragment extends Fragment {
	public static final String EXTRA_CRIME_ID = "com.bignerdranch.android.criminalintent.crime_id";
	public static final String DIALOG_DATE = "date";
	public static final String DIALOG_TIME = "time";
	public static final int REQUEST_DATE = 0;
	public static final int REQUEST_TIME = 1;
	public static final String TAG = "TimePickerFragment";

	
	private Crime mCrime;
	private EditText mTitleField;
	private Button mDateButton;
	private Button mTimeButton;
	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() {
			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
			}
		});
		
		mDateButton = (Button)v.findViewById(R.id.crime_date);
		mDateButton.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				FragmentManager fm = getActivity().getSupportFragmentManager();
				DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
				dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
				dialog.show(fm, DIALOG_DATE);
			}
		});
		
		mTimeButton = (Button)v.findViewById(R.id.crime_time);
		mTimeButton.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				FragmentManager fm = getActivity().getSupportFragmentManager();
				TimePickerFragment dialog = TimePickerFragment.newInstance(mCrime.getDate());
				dialog.setTargetFragment(CrimeFragment.this, REQUEST_TIME);
				dialog.show(fm, DIALOG_TIME);
			}
		});
		
		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);
			}
		});
		
		updateDate();

		return v;
	}
	
	@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);
			updateDate();
		}
		if (requestCode == REQUEST_TIME) {
			Date date = (Date)data.getSerializableExtra(TimePickerFragment.EXTRA_TIME);
			mCrime.setDate(date);
			updateDate();
		}
	}
	
	@SuppressLint("SimpleDateFormat")
	public void updateDate() {
		SimpleDateFormat datFormat = new SimpleDateFormat("MMM dd, yyyy");
		SimpleDateFormat timFormat = new SimpleDateFormat("kk:mm:ss zzz");
		mDateButton.setText(datFormat.format(mCrime.getDate()));
		mTimeButton.setText(timFormat.format(mCrime.getDate()));
	}
	
	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;
	}

}

#2

Yeah, I’m not seeing an easy answer for you right now. I’ll need to put together a test project - I’ll get back to you.


#3

Here’s the fix:

1- Stash your TimePicker in an instance variable.

2- Call setCurrentHour(0) on it inside onCreateView.

3- Set the actual minute and hour inside onResume.

There is no sensible explanation I can give you as to why this is a sensible fix. The original behavior is just buggy. But the fix does work.


#4

Thanks! Taking your lead, I put the TimePicker object into a member (instance) variable. From there, simply ‘resetting’ the current set hour inside onResume() did the trick. Here are the relevant changes to my original TimePickerFragment post. Thanks for the help! Much appreciated.

	private TimePicker mTimePicker;
	
	@Override
	public void onResume() {
		super.onResume();
		// Reset the displayed current Hour - pre-Honeycomb rotation bug fix
		int hourOfDay = mTimePicker.getCurrentHour(); 
		mTimePicker.setCurrentHour(hourOfDay);
	}
	
	@Override
	public Dialog onCreateDialog(Bundle savedInstanceState) {
		...
		mTimePicker = (TimePicker)v.findViewById(R.id.dialog_date_timePicker);
		mTimePicker.setIs24HourView(true);
		mTimePicker.setCurrentHour(hourOfDay);
		mTimePicker.setCurrentMinute(minute);
		mTimePicker.setOnTimeChangedListener(new OnTimeChangedListener() {
			...
			}
		});
		...
	}