Search code examples
androidsettingsandroid-timepickerpreferencescreendialog-preference

Appcompatactivity with custom native (not compatibility) dialogpreference containing a TimePicker


I am building a preferences / settings screen inside an Android AppCompatActivity. One requirement is a custom [DialogPreference][1] with a TimePicker.

The DialogPreference must be 'native', meaning not the compatibility version like described here and here.

The code for the AppCompatActivity:

...

public class SettingsActivity extends AppCompatActivity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_preferences);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_settings);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }
}

The layout of activity_preferences.xml:

...

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <fragment
            android:name="nl.waywayway.broodkruimels.SettingsFragment"
            android:id="@+id/settings_fragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </android.support.v4.widget.NestedScrollView>

The SettingsFragment class:

...

public class SettingsFragment extends PreferenceFragment
{
    Context mContext;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.preferences);
    }
}

The preferences.xml file:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SwitchPreference
        android:key="pref_notify"
        android:title="@string/pref_notify"
        android:summary="@string/pref_notify_summ"
        android:defaultValue="false" />

    <nl.waywayway.broodkruimels.TimePreference
        android:dependency="pref_notify"
        android:key="pref_notify_time"
        android:title="@string/pref_notify_time"
        android:summary="@string/pref_notify_time_summ"
        android:defaultValue="390" />

</PreferenceScreen>

And the custom TimePreference class:

public class TimePreference extends DialogPreference
{
    private TimePicker mTimePicker = null;
    private int mTime;
    private int mDialogLayoutResId = R.layout.preferences_timepicker_dialog;

    // 4 constructors for the API levels,
    // calling each other

    public TimePreference(Context context)
    {
        this(context, null);
    }

    public TimePreference(Context context, AttributeSet attrs)
    {
        this(context, attrs, R.attr.preferenceStyle);
    }

    public  TimePreference(Context context, AttributeSet attrs, int defStyleAttr)
    {
        this(context, attrs, defStyleAttr, defStyleAttr);
    }

    public  TimePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
    {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public int getTime()
    {
        return  mTime;
    }

    public void setTime(int time)
    {
        mTime = time;

        // Save to Shared Preferences
        persistInt(time);
    }

    @Override
    public int getDialogLayoutResource()
    {
        return mDialogLayoutResId;
    }

    @Override
    protected Object onGetDefaultValue(TypedArray a, int index)
    {
        //  Default  value  from  attribute.  Fallback  value  is  set  to  0.
        return a.getInt(index,  0);
    }

    @Override
    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue)
    {
        // Read the value. Use the default value if it is not possible.
        setTime(restorePersistedValue ?
                getPersistedInt(mTime) : (int) defaultValue);
    }

    @Override
    protected void onBindDialogView(View view)
    {
        super.onBindDialogView(view);

        mTimePicker = (TimePicker) view.findViewById(R.id.preferences_timepicker);

        if (mTimePicker == null)
        {
            throw new IllegalStateException("Dialog view must contain a TimePicker with id 'preferences_timepicker'");
        }

        // Get the time from the related Preference
        Integer minutesAfterMidnight = null;
        TimePreference preference = (TimePreference) findPreferenceInHierarchy("pref_notify_time");
        minutesAfterMidnight = preference.getTime();

        // Set the time to the TimePicker
        if (minutesAfterMidnight != null)
        {
            int hours = minutesAfterMidnight / 60;
            int minutes = minutesAfterMidnight % 60;
            boolean is24hour = DateFormat.is24HourFormat(getContext());

            mTimePicker.setIs24HourView(is24hour);

            if (Build.VERSION.SDK_INT >= 23)
            {
                mTimePicker.setHour(hours);
                mTimePicker.setMinute(minutes);
            }
            else
            {
                mTimePicker.setCurrentHour(hours);
                mTimePicker.setCurrentMinute(minutes);
            }
        }
    }

    @Override
    protected void onDialogClosed(boolean positiveResult)
    {
        if (positiveResult)
        {
            // Get the current values from the TimePicker
            int hours;
            int minutes;
            if (Build.VERSION.SDK_INT >= 23)
            {
                hours = mTimePicker.getHour();
                minutes = mTimePicker.getMinute();
            }
            else
            {
                hours = mTimePicker.getCurrentHour();
                minutes = mTimePicker.getCurrentMinute();
            }

            // Generate value to save
            int minutesAfterMidnight = (hours * 60) + minutes;

            // Save the value
            TimePreference timePreference = (TimePreference) findPreferenceInHierarchy("pref_notify_time");

            // This allows the client to ignore the user value.
            if (timePreference.callChangeListener(minutesAfterMidnight))
            {
                // Save the value
                timePreference.setTime(minutesAfterMidnight);
            }
        }
    }
}

The preferences_timepicker_dialog.xml file is as follows:

...
<TimePicker 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/preferences_timepicker"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

The result is like the screenshot below. On a Moto G5 plus phone with Android 7.

Screenshot

Question: There should be two preferences. However, the custom DialogPreference is not showing in the settings list. What is going wrong here? Does the AppCompatActivity actually work with the 'native' DialogPreference?

The TimePreference class is actually instantiated from the preferences xml, that could be logged from the constructor. Also no compile time errors, no runtime errors.


Solution

  • Finally I found a different approach that looks clean, tested and works on real devices from Android 4 until 7. The Preference is showing in the Preference screen.

    Also, the TimePicker dialog is properly showing in landscape orientation. This is a problem on some devices. See

    Steps are:

    The preferences Activity:

    ...
    public class SettingsActivity extends AppCompatActivity
    {
        public static final String KEY_PREF_NOTIFY = "pref_notify";
        public static final String KEY_PREF_NOTIFY_TIME = "pref_notify_time";
    
        @Override
        protected void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_preferences);
        }
    }
    

    The layout file activity_preferences.xml:

    ...
    
        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">
    
            <fragment
                android:name="nl.waywayway.broodkruimels.SettingsFragment"
                android:id="@+id/settings_fragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
    
        </android.support.v4.widget.NestedScrollView>
    

    The SettingsFragment class:

    ...
    public class SettingsFragment extends PreferenceFragmentCompat
    {
        Context mContext;
    
        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey)
        {
            setPreferencesFromResource(R.xml.preferences, rootKey);
        }
    
        // The Context object of this fragment is only available when this fragment is 'attached', so set the Context object inside the onAttach() method
        @Override
        public void onAttach(Context context)
        {
            super.onAttach(context);
            mContext = context;
        }
    
        // This method sets the action of clicking the Preference
        @Override
        public boolean onPreferenceTreeClick(Preference preference)
        {
            switch (preference.getKey())
            {
                case SettingsActivity.KEY_PREF_NOTIFY_TIME:
                    showTimePickerDialog(preference);
                    break;
            }
    
            return super.onPreferenceTreeClick(preference);
        }
    
        private void showTimePickerDialog(Preference preference)
        {
            DialogFragment newFragment = new TimePickerFragment();
            newFragment.show(getFragmentManager(), "timePicker");
        }
    }
    

    The preferences.xml file:

    ...
    <android.support.v7.preference.PreferenceScreen
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <android.support.v7.preference.SwitchPreferenceCompat
            android:key="pref_notify"
            android:title="@string/pref_notify"
            android:summary="@string/pref_notify_summ"
            android:defaultValue="false" />
    
        <android.support.v7.preference.Preference
            android:dependency="pref_notify"
            android:key="pref_notify_time"
            android:title="@string/pref_notify_time"
            android:summary="@string/pref_notify_time_summ"
            android:defaultValue="390" />
    
    </android.support.v7.preference.PreferenceScreen>
    

    The TimePickerFragment class, see the Android 'Pickers' guide (https://developer.android.com/guide/topics/ui/controls/pickers.html) for an explanation:

    ...
    public class TimePickerFragment extends DialogFragment
        implements TimePickerDialog.OnTimeSetListener
    {
        private Context mContext;
        private int mTime; // The time in minutes after midnight
    
        // The Context object for this fragment is only available when this fragment is 'attached', so set the Context object inside the onAttach() method
        @Override
        public void onAttach(Context context)
        {
            super.onAttach(context);
            mContext = context;
        }
    
        // Getter and setter for the time
        public int getTime()
        {
            SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext);
            int prefDefault = mContext.getResources().getInteger(R.integer.preferences_time_default);
            mTime = sharedPref.getInt(SettingsActivity.KEY_PREF_NOTIFY_TIME, prefDefault);
    
            return  mTime;
        }
    
        public void setTime(int time)
        {
            mTime = time;
    
            // Save to Shared Preferences
            SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(mContext);
            SharedPreferences.Editor editor = sharedPref.edit();
            editor.putInt(SettingsActivity.KEY_PREF_NOTIFY_TIME, time);
            editor.commit();
        }
    
        public Dialog onCreateDialog(Bundle savedInstanceState)
        {
            int minutesAfterMidnight = getTime();
            int hour = minutesAfterMidnight / 60;
            int minute = minutesAfterMidnight % 60;
    
            Log.i("HermLog", "onCreateDialog(), tijd: " + hour + ":" + minute);
    
            // Create a new instance of TimePickerDialog and return it
            return new TimePickerDialog(
                mContext, 
                this, 
                hour, 
                minute,
                DateFormat.is24HourFormat(mContext)
            );
        }
    
        @Override
        public void onTimeSet(TimePicker view, int hour, int minute)
        {
            int minutesAfterMidnight = (hour * 60) + minute;
            setTime(minutesAfterMidnight);      
        }
    }