Skip to content

Instantly share code, notes, and snippets.

@jeffdgr8
Last active February 14, 2023 13:15
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jeffdgr8/6bc5f990bf0c13a7334ce385d482af9f to your computer and use it in GitHub Desktop.
Save jeffdgr8/6bc5f990bf0c13a7334ce385d482af9f to your computer and use it in GitHub Desktop.
TimePickerDialog with fixed android:timePickerMode spinner in Nougat
package my.packagename;
import android.app.TimePickerDialog;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.TimePicker;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
/**
* Workaround for this bug: https://code.google.com/p/android/issues/detail?id=222208
* In Android 7.0 Nougat, spinner mode for the TimePicker in TimePickerDialog is
* incorrectly displayed as clock, even when the theme specifies otherwise, such as:
*
* <resources>
* <style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar">
* <item name="android:timePickerStyle">@style/Widget.MyApp.TimePicker</item>
* </style>
*
* <style name="Widget.MyApp.TimePicker" parent="android:Widget.Material.TimePicker">
* <item name="android:timePickerMode">spinner</item>
* </style>
* </resources>
*
* May also pass TimePickerDialog.THEME_HOLO_LIGHT as an argument to the constructor,
* as this theme has the TimePickerMode set to spinner.
*/
public class TimePickerDialogFixedNougatSpinner extends TimePickerDialog {
/**
* Creates a new time picker dialog.
*
* @param context the parent context
* @param listener the listener to call when the time is set
* @param hourOfDay the initial hour
* @param minute the initial minute
* @param is24HourView whether this is a 24 hour view or AM/PM
*/
public TimePickerDialogFixedNougatSpinner(Context context, OnTimeSetListener listener, int hourOfDay, int minute, boolean is24HourView) {
super(context, listener, hourOfDay, minute, is24HourView);
fixSpinner(context, hourOfDay, minute, is24HourView);
}
/**
* Creates a new time picker dialog with the specified theme.
*
* @param context the parent context
* @param themeResId the resource ID of the theme to apply to this dialog
* @param listener the listener to call when the time is set
* @param hourOfDay the initial hour
* @param minute the initial minute
* @param is24HourView Whether this is a 24 hour view, or AM/PM.
*/
public TimePickerDialogFixedNougatSpinner(Context context, int themeResId, OnTimeSetListener listener, int hourOfDay, int minute, boolean is24HourView) {
super(context, themeResId, listener, hourOfDay, minute, is24HourView);
fixSpinner(context, hourOfDay, minute, is24HourView);
}
private void fixSpinner(Context context, int hourOfDay, int minute, boolean is24HourView) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N) { // fixes the bug in API 24
try {
// Get the theme's android:timePickerMode
final int MODE_SPINNER = 1;
Class<?> styleableClass = Class.forName("com.android.internal.R$styleable");
Field timePickerStyleableField = styleableClass.getField("TimePicker");
int[] timePickerStyleable = (int[]) timePickerStyleableField.get(null);
final TypedArray a = context.obtainStyledAttributes(null, timePickerStyleable, android.R.attr.timePickerStyle, 0);
Field timePickerModeStyleableField = styleableClass.getField("TimePicker_timePickerMode");
int timePickerModeStyleable = timePickerModeStyleableField.getInt(null);
final int mode = a.getInt(timePickerModeStyleable, MODE_SPINNER);
a.recycle();
if (mode == MODE_SPINNER) {
TimePicker timePicker = (TimePicker) findField(TimePickerDialog.class, TimePicker.class, "mTimePicker").get(this);
Class<?> delegateClass = Class.forName("android.widget.TimePicker$TimePickerDelegate");
Field delegateField = findField(TimePicker.class, delegateClass, "mDelegate");
Object delegate = delegateField.get(timePicker);
Class<?> spinnerDelegateClass;
spinnerDelegateClass = Class.forName("android.widget.TimePickerSpinnerDelegate");
// In 7.0 Nougat for some reason the timePickerMode is ignored and the delegate is TimePickerClockDelegate
if (delegate.getClass() != spinnerDelegateClass) {
delegateField.set(timePicker, null); // throw out the TimePickerClockDelegate!
timePicker.removeAllViews(); // remove the TimePickerClockDelegate views
Constructor spinnerDelegateConstructor = spinnerDelegateClass.getConstructor(TimePicker.class, Context.class, AttributeSet.class, int.class, int.class);
spinnerDelegateConstructor.setAccessible(true);
// Instantiate a TimePickerSpinnerDelegate
delegate = spinnerDelegateConstructor.newInstance(timePicker, context, null, android.R.attr.timePickerStyle, 0);
delegateField.set(timePicker, delegate); // set the TimePicker.mDelegate to the spinner delegate
// Set up the TimePicker again, with the TimePickerSpinnerDelegate
timePicker.setIs24HourView(is24HourView);
timePicker.setCurrentHour(hourOfDay);
timePicker.setCurrentMinute(minute);
timePicker.setOnTimeChangedListener(this);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private static Field findField(Class objectClass, Class fieldClass, String expectedName) {
try {
Field field = objectClass.getDeclaredField(expectedName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {} // ignore
// search for it if it wasn't found under the expected ivar name
for (Field searchField : objectClass.getDeclaredFields()) {
if (searchField.getType() == fieldClass) {
searchField.setAccessible(true);
return searchField;
}
}
return null;
}
}
@johnshine
Copy link

Can you send me the whole project?
This is my gmail account,shinethawka8800@gmail.com.

@uqmessias
Copy link

Thanks @jeffdgr8, for taking the time to post it here.

I forked your original gist and add a new file into it to fix the same issue on DatePicker. But instead of using the DatePickerSpinnerDelegate constructor directly (what didn't work for some reason), I just called the DatePicker.createSpinnerUIDelegate (private method) and it worked for me!

This is the fork, if anyone is facing the same issue!

@jeffdgr8
Copy link
Author

jeffdgr8 commented Sep 4, 2018

@lognaturel sorry for not replying earlier. I defined a permissive software license for all my gists now: https://gist.github.com/jeffdgr8/ccc2416343e2f5d1e86101524df9630c Feel free to use!

@jeffdgr8
Copy link
Author

jeffdgr8 commented Sep 4, 2018

For those finding the need to change MODE_SPINNER to 2, most likely you have not properly defined the time picker mode to spinner mode.

As I describe in the class comment, this can be done in your custom theme as shown. A android:timePickerStyle style needs to have android:timePickerMode defined as spinner.

Alternatively, you can pass a theme that has the time picker mode set to spinner mode, such as TimePickerDialog.THEME_HOLO_LIGHT in the TimePickerDialogFixedNougatSpinner constructor as the themeResId argument.

If you look at the definition of TimePicker you can see these constants defined:

public static final int MODE_SPINNER = 1
Presentation mode for the Holo-style time picker that uses a set of NumberPickers.

public static final int MODE_CLOCK = 2
Presentation mode for the Material-style time picker that uses a clock face.

You can use getMode() on TimePicker to check what it's set to:

public int getMode()
Returns: the picker's presentation mode, one of MODE_CLOCK or MODE_SPINNER

This class is meant as a fix for a bug in 7.0 Nougat's TimePickerDialog implementation, which fails to provide MODE_SPINNER functionality. For all other platform versions, the default TimePickerDialog will display spinner mode correctly if properly defined in your app theme or a theme constructor argument. Changing MODE_SPINNER to 2 in this code just forces spinner mode even when your theme is set to clock mode.

@Seif0o0
Copy link

Seif0o0 commented Jun 12, 2019

hello ...
my app crashes while using ur code in android 9 ..
This line is giving a NoSuchFieldException
Field timePickerStyleableField = styleableClass.getField("TimePicker");
but when i remove this line throw new RuntimeException(e);
app doesn't crash but time picker change from spinner to clock mode
can any one give me a hand ?

@anees17861
Copy link

Hi @jeffdgr8. This is working perfectly on nougat once I change MODE_SPINNER to 2. But it is causing a crash on pie device
https://gist.github.com/jeffdgr8/6bc5f990bf0c13a7334ce385d482af9f#file-timepickerdialogfixednougatspinner-java-L70

Field timePickerStyleableField = styleableClass.getField("TimePicker");

This line is giving a NoSuchFieldException.
Is it safe to assume that your fix needs to be applied to only nougat devices and for other devices providing theme as THEME_HOLO_LIGHT is enough. My app requires minSdk to be lollipop, so that condition seems to be satisfied as well

@jeffdgr8
Copy link
Author

jeffdgr8 commented Jun 13, 2019

@Seif0o0 @anees17861 this bug was fixed in API 25 (https://issuetracker.google.com/issues/37119315), so the fix only needs to be applied to API 24. I adjusted the code such that it will only run on API 24 devices. Let me know if there are any issues with the latest version.

@naveedahmad99
Copy link

naveedahmad99 commented Jul 25, 2019

hello ...
my app crashes while using ur code in android 9 ..
This line is giving a NoSuchFieldException
Field timePickerStyleableField = styleableClass.getField("TimePicker");
but when i remove this line throw new RuntimeException(e);
app doesn't crash but time picker change from spinner to clock mode
can any one give me a hand ?

this fixed my issue as app is not crashing now on Android 9

@HurJungUn
Copy link

thx!!!

@pravi4444
Copy link

for Class<?> classForid = Class.forName("com.android.internal.R$id");
line
we are getting below warning and app is crashing.

accessing internal APIs via reflection is not supported and may not work on all devices or in the future less... (⌘F1)
Using reflection to access hidden/private Android APIs is not safe; it will often not work on devices from other vendors, and it may suddenly stop working (if the API is removed) or crash spectacularly (if the API behavior changes, since there are no guarantees for compatibility.)
https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces

Any other guys facing this issue?
Thanks,

@pravi4444
Copy link

hello ...
my app crashes while using ur code in android 9 ..
This line is giving a NoSuchFieldException
Field timePickerStyleableField = styleableClass.getField("TimePicker");
but when i remove this line throw new RuntimeException(e);
app doesn't crash but time picker change from spinner to clock mode
can any one give me a hand ?

this fixed my issue as app is not crashing now on Android 9

by commenting this line its hiding the error but how you are getting/reading values from mTimePicker as its null in onClick method.
While reading not able to get the values as mTimePicker is null.

Thanks.

@pravi4444
Copy link

for Class<?> classForid = Class.forName("com.android.internal.R$id");
line
we are getting below warning and app is crashing.

accessing internal APIs via reflection is not supported and may not work on all devices or in the future less... (⌘F1)
Using reflection to access hidden/private Android APIs is not safe; it will often not work on devices from other vendors, and it may suddenly stop working (if the API is removed) or crash spectacularly (if the API behavior changes, since there are no guarantees for compatibility.)
https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces

Any other guys facing this issue?
Thanks,

Its working on API level 28 and below but crashing on Android 9 and Android 10

@jeffdgr8
Copy link
Author

for Class<?> classForid = Class.forName("com.android.internal.R$id");
line
we are getting below warning and app is crashing.
accessing internal APIs via reflection is not supported and may not work on all devices or in the future less... (⌘F1)
Using reflection to access hidden/private Android APIs is not safe; it will often not work on devices from other vendors, and it may suddenly stop working (if the API is removed) or crash spectacularly (if the API behavior changes, since there are no guarantees for compatibility.)
https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces
Any other guys facing this issue?
Thanks,

Its working on API level 28 and below but crashing on Android 9 and Android 10

The TimePickerDialogFixedNougatSpinner class behaves exactly as the super TimePickerDialog class on all platform versions except for API 24 Nougat. The bug this addresses is only in that platform version. You can see at the top of the fixSpinner() method, it checks the platform version and only executes the workaround code if the platform version is N. I just tested and confirmed, the code works fine on Android 10. Do you have a stack trace for the crash you're experiencing?

While there are a few places in the code that call the method Class.forName(), none of them are accessing com.android.internal.R$id, so I'm not sure where you got that specific line of code from. The warning is expected, as the lint tool will detect this type of code utilizing reflection to access an internal API. Since the internal API is broken, that's why we need to access it to fix the bug. But since this code will only execute specifically on the buggy API 24 platform, it's guaranteed to work as expected, even as future API versions potentially modify the internal API, since it won't be executing the code on these API versions.

@vonovak
Copy link

vonovak commented Jan 30, 2020

@jeffdgr8 thanks for this. Just wondering, is there any reason the if (mode == MODE_SPINNER) condition is not included on the same line as if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N)? Doesn't seem like the reflection calls above line 77 have any needed side effects..?

@jeffdgr8
Copy link
Author

@vonovak the reflection calls on lines 69-75 are all to get the mode to compare on line 77, since we only need to perform the workaround if we are in MODE_SPINNER. The API level check on line 65 happens before all the reflection calls, so that it doesn't need to make those reflection calls or do any of the workaround code for any API level, other than the buggy API 24.

@vonovak
Copy link

vonovak commented Jan 31, 2020

ah, right 🤦‍♂️ thank you

@putrabangga
Copy link

hei @jeffdgr8 i've problem with this code when running on Android 10, device pixel 2. when i trace appear an error like this java.lang.NoSuchMethodException: android.widget.TimePickerSpinnerDelegate is it because TimePickerSpinnerDelegate no longer on android 10? thanks.

@jeffdgr8
Copy link
Author

@putrabangga are you using the latest version of the code? The workaround will only execute on Android Nougat, API 24. So it shouldn't be an issue on Android 10.

@putrabangga
Copy link

so i can't use spinner time picker on Android 10? @jeffdgr8

@jeffdgr8
Copy link
Author

@putrabangga yes, you can. But there isn't a bug that needs to be worked around with it. Simply enable spinner mode in your theme, as described in the comment at the top of the code. Using this TimePickerDialogFixedNougatSpinner class works around the bug in Android Nougat, and otherwise functions as the default TimePickerDialog, including spinner mode according to the theme.

@CyprienGanassali
Copy link

Thanks for your gist @jeffdrg8, It works on Android N and higher, but for me on Android M, implementation of TimePickerDialog.OnTimeSetListener doesn't fire. Any workaround ?

@jeffdgr8
Copy link
Author

@CyprienGanassali I'm not aware of an issue with Android M. This code should behave as the default TimePickerDialog on all versions of Android except Android N though. The workaround code in fixSpinner() is only executed on Android N to fix this specific issue with that API level. Does your code work as expected on Android M when using the default TimePickerDialog?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment