-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[TIMOB-8430]Android: fix google issue with am/pm #8799
Conversation
@fmerzadyan, Is this expected ? |
import android.os.Build; | ||
import android.widget.TimePicker; | ||
import android.widget.TimePicker.OnTimeChangedListener; | ||
import com.frankify.f; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?!
89b2df1
to
a36153b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In TiUITimePicker.java
@Override
public void onTimeChanged(TimePicker view, int hourOfDay, int minute)
{
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, hourOfDay);
calendar.set(Calendar.MINUTE, minute);
// Make sure .value is readable by user
proxy.setProperty("value", calendar.getTime());
if (!suppressChangeEvent) {
KrollDict data = new KrollDict();
data.put("value", calendar.getTime());
fireEvent("change", data);
}
}
public void run() { | ||
detectTimeChange(); | ||
} | ||
}, delay, rate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use a Java "Timer". It executes the TimerTask on a separate thread, which will cause race conditions in our code. Use the Android "Handler" class instead which runs on the main UI thread (if created on the main thread). You'll want to use its postDelayed() method, but you'll have to call postDelayed() within the Runnable so that it'll schedule itself again.
Also, your current timer will run forever. It'll continue to run after the view has been removed (this is bad). You should start posting with the handler when the View.onAttachedToWindow() method gets called and stop the handler when the View.onDetachedFromWindow() method gets called.
https://developer.android.com/reference/android/view/View.html#onAttachedToWindow()
https://developer.android.com/reference/android/view/View.html#onDetachedFromWindow()
TimePicker picker = (TimePicker) getNativeView(); | ||
boolean hasChanged; | ||
if (Build.VERSION.SDK_INT >= 23) { | ||
hasChanged = lastSelectedHour != picker.getHour() || lastSelectMin != picker.getMinute(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause an "opcode" warning in the log when ran on an API Level 22 or lower device. Please change it to the following...
private void detectTimeChange()
{
TimePicker picker = (TimePicker) getNativeView();
if (picker == null) {
return;
}
int currentSelectedHour;
int currentSelectedMinute;
if (Build.VERSION.SDK_INT >= 23) {
currentSelectedHour = ApiLevel23.getHourFrom(picker);
currentSelectedMinute = ApiLevel23.getMinuteFrom(picker);
} else {
currentSelectedHour = picker.getCurrentHour();
currentSelectedMinute = picker.getCurrentMinute();
}
// ...
}
private static class ApiLevel23
{
private ApiLevel23() {}
public static int getHourFrom(TimePicker picker)
{
return picker.getHour();
}
public static int getMinuteFrom(TimePicker picker)
{
return picker.getMinute();
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any warnings running this on my Android 5.1 emulator. I don't think this is necessary, the if
guard should prevent lower devices from executing unsupported code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any why do we need 2 if-checks for each API level? Should do that more compact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@garymathews, the "opcode" warnings are definitely happening. They can be seen via "adb logcat". Our existing codebase has this issue everywhere and there is a desire to fix it per the following JIRA case...
https://jira.appcelerator.org/browse/TIMOB-20258
The solution I posted above resolves it by taking advantage of Java's lazy class loading, which is the recommended solution by Google (link below) and is the same solution Google uses in their Android "support" libraries.
https://android-developers.googleblog.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hansemannn, yeah, I wrote the code within the github inline commenter and didn't notice I did that (d'oh). I just updated the above code snippet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally fine :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These warnings are only thrown from Dalvik which is why I didn't see them (and won't be seen on Android 5.0+ due to moving to ART). These warnings are expected behaviour, but ugly. Since the references are properly guarded no issue will actually come from this.
If we do want to address this we should do it with a class in the appcompat
module instead of a nested class. Also note that it doesn't provide any additional safety as it still relies on the
if (Build.VERSION.SDK_INT ...
statement. It's just preventing Dalvik from seeing the broken reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't agree. For one-off API calls like this, we should use an inner class as I've shown above. It keeps it simple and it keeps the code together in one place... making it easier to maintain and understand. And the ApiLevel23 class name makes it clear to the developer that it should only be accessed by that API level or higher. Also, by making it an inner class, it effectively becomes a private API that module developers cannot use (this is intentional; we're hding the complexity from a public API standpoint).
Moving higher API level calls to another class is only useful if it's needed by multiple classes. Let's avoid this unless needed, because once we create a public class in an open source project, it's set in stone. We'll have to maintain it and provide tech-support for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jquick-axway I agree if it were just a couple of instances, but if we plan on suppressing all of the warnings from our code base like you mentioned with TIMOB-20258 then it would be more maintainable to create a single class for each API level needed (23, 21, 16
) in an appropriate place and reference to that; than having nested classes in each class with an unsupported API reference.
Here's a list of references we need to "hide" from Dalvik:
AndroidModule.java
API-23
- L365: checkSelfPermission
- L389: checkSelfPermission
- L396: requestPermissions
CalendarProxy.java
API-23
- L105: checkSelfPermission
- L106: checkSelfPermission
CommonContactsApi.java
API-23
- L72: checkSelfPermission
FilesystemModule.java
API-23
- L94: checkSelfPermission
- L110: requestPermissions
GeolocationModule.java
API-23
- L623: checkSelfPermission
- L645: requestPermissions
MediaModule.java
API-23
- L359: checkSelfPermission
- L360: checkSelfPermission
- L371: checkSelfPermission
- L382: checkSelfPermission
TiUIWebView.java
- API-16
- L331: setAllowUniversalAccessFromFileURLs
TiWebChromeClient.java
API-23
- L229: checkSelfPermission
TiWindowProxy.java
API-21
- L531: makeSceneTransitionAnimation
WindowProxy.java
API-16
- L162: startActivity
API-21
- L176: finishAfterTransition
- L493: setEnterTransition, createTransition
- L497: setExitTransition, createTransition
- L501: setReturnTransition, createTransition
- L505: setReenterTransition, createTransition
- L509: setSharedElementEnterTransition, createTransition
- L513: setSharedElementExitTransition, createTransition
- L517: setSharedElementReenterTransition, createTransition
- L521: setSharedElementReturnTransition, createTransition
TiViewProxy.java
API-21
- L802: createCircularReveal
- L836: createCircularReveal
TiBaseActivity.java
API-23
- L619: finishAndRemoveTask
- L881: finishAffinity
TiActivitySupportHelper.java
API-16
- startIntentSenderForResult
TiFileHelper2.java
API-23
- L94: checkSelfPermission
TiBorderWrapperView.java
API-21
- L128: ViewOutlineProvider
TiUIView.java
API-16
- L545: removeOnGlobalLayoutListener
- L628: removeOnGlobalLayoutListener
I don't like the idea of creating a nested class for each of those instances. We could do something like this:
org.appcelerator.titanium.compat
Api23.java
Api21.java
Api16.java
These classes would be for internal use only, undocumented and not to be used by module developers.
However, I did notice even the Google support libraries do not suppress all of their warnings:
Could not find method android.content.res.Resources.getDrawable, referenced from method android.support.v7.widget.ResourcesWrapper.getDrawable
VFY: unable to resolve virtual method 565: Landroid/content/res/Resources;.getDrawable (ILandroid/content/res/Resources$Theme;)Landroid/graphics/drawable/Drawable;
VFY: replacing opcode 0x6e at 0x0002
Could not find method android.content.res.Resources.getDrawableForDensity, referenced from method android.support.v7.widget.ResourcesWrapper.getDrawableForDensity
VFY: unable to resolve virtual method 567: Landroid/content/res/Resources;.getDrawableForDensity (IILandroid/content/res/Resources$Theme;)Landroid/graphics/drawable/Drawable;
VFY: replacing opcode 0x6e at 0x0002
Could not find class 'android.support.graphics.drawable.VectorDrawableCompat', referenced from method android.support.v7.widget.AppCompatDrawableManager.isVectorDrawable
VFY: unable to resolve instanceof 365 (Landroid/support/graphics/drawable/VectorDrawableCompat;) in Landroid/support/v7/widget/AppCompatDrawableManager;
VFY: replacing opcode 0x20 at 0x0000
Could not find method android.support.graphics.drawable.VectorDrawableCompat.createFromXmlInner, referenced from method android.support.v7.widget.AppCompatDrawableManager$VdcInflateDelegate.createFromXmlInner
VFY: unable to resolve static method 1631: Landroid/support/graphics/drawable/VectorDrawableCompat;.createFromXmlInner (Landroid/content/res/Resources;Lorg/xmlpull/v1/XmlPullParser;Landroid/util/AttributeSet;Landroid/content/res/Resources$Theme;)Landroid/support/graphics/drawable/VectorDrawableCompat;
VFY: replacing opcode 0x71 at 0x0004
Could not find method android.support.graphics.drawable.AnimatedVectorDrawableCompat.createFromXmlInner, referenced from method android.support.v7.widget.AppCompatDrawableManager$AvdcInflateDelegate.createFromXmlInner
VFY: unable to resolve static method 1630: Landroid/support/graphics/drawable/AnimatedVectorDrawableCompat;.createFromXmlInner (Landroid/content/Context;Landroid/content/res/Resources;Lorg/xmlpull/v1/XmlPullParser;Landroid/util/AttributeSet;Landroid/content/res/Resources$Theme;)Landroid/support/graphics/drawable/AnimatedVectorDrawableCompat;
VFY: replacing opcode 0x71 at 0x0004
- What are your thoughts for this approach?
- Is it worthwhile to suppress these warnings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't create public "ApiLevelX" classes because that shifts the responsibility to the user of our classes to implement the correct version fallback behavior. If we were to implement something like this, I think Google's "DrawableCompat" class (link below) would be a better way to do it, because it buries/hides the complexity from the users of our library/classes. With the one difference is that I wouldn't use dessert names like Google. I would name the classes based on API level number instead like I did above, which is less error prone. Especially since Google's API docs displays an API level number next to every API. (I've seen devs make bad assumptions about which dessert maps to which API level number, causing a crash.)
https://github.com/guardianproject/android-support-library/blob/master/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
But that said, my preference is to not create public bags of static functions under the AppCompat package/namespace. I think it makes more sense to group higher API levels calls by feature under the same package that uses them like "network", "ui", "view", etc. For example, a developer would normally expect all network related classes to be under the network namespace/package. Also, I prefer to keep the higher API level accessing classes as private inner-classes because this makes it more future proof. So, when we do raise the min API level we support in the future, we can refactor the obsolete API level accessing private inner-classes without it being a breaking change. This also means that I think wrapping Android features in our own classes is the way to go so that users of our classes don't have to worry about API levels, like how it works with many of Google's support library classes.
Regarding the TimePicker case above, since no other class in our code needs to call those 2 methods, it's simpler from a productivity standpoint to bury the higher level API accessor as an inner class like I've done it. This reduces the spaghetti code-ness of our library (it's only 14 lines of code). But if we ever need to rip it out in the future, then we can easily do so without it being a breaking change since it's a private inner class.
I don't expect us to fix all of these opcodes issues in one shot. These issues are everywhere and is too big of a job. Fixing these issues a bit at a time as we make changes is probably the way to go and we should strive to avoid adding new opcode warnings. I think this is a low priority issue since I've never heard of Google Play app reviewers reject apps due to opcode warnings (apps to be featured on Google Play actually go through an app review process). This is more of a tech-support issue on our end.
Oh and if you think the solution to solving these opcode warning is a hack, well, you'd be right. :-) But it's a Google sanctioned hack that Google's own support libraries 100% depend on.
@@ -33,6 +35,9 @@ | |||
protected Date minDate, maxDate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the "suppressChangeEvent" member variable.
Modify the setValue() method to update the "lastSelectedHour" and "lastSelectedMin" member variables before updating the TimePicker object. This way, when TimePicker.onTimeChanged() method gets invoked, your detectTimeChange() method will not fire the time changed event because the picker's selected time will already match your member variables.
5a12e47
to
2d49b7e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put the "Override" back on the ontTimeChanged() method please?
Other than that, the changes look good!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CR: Pass
FR Failed. On Android 5.1.1. & 6.0.1 the change event does not fire when I change From AM to PM & vice versa. But, if you change from e.g AMtoPM, I have to tap on the hour or minute for the change event to fire. |
7ed1361
to
793fc27
Compare
I strongly prefer #9026's simpler approach to the one used here. I think if/when that PR passes FR, we should merge it and cherry-pick to master. However, I do think we should incorporate some of the code cleanup from this PR on master branch where the string literals were replaced with proper constant references for property and event names (such as the changes to |
FR Passed. Switching between am and pm fires the change event successfully every time. Studio Ver: 4.9.0.201705170123 |
Test Case
label should show the same time, in 24 hour format, as the time picker when toggling "AM" or "PM".
JIRA: https://jira.appcelerator.org/browse/TIMOB-8430