Status Update
Comments
am...@google.com <am...@google.com>
am...@google.com <am...@google.com> #2
Branch: androidx-main
commit 2db6d01af84b5752190c6dd28cdea12500b701cb
Author: Charcoal Chen <charcoalchen@google.com>
Date: Fri Dec 23 10:15:10 2022
Fix JPEG image corruption issue if writing Exif location data on some Samsung Android 12 devices
This issue can be avoided in CameraX side by skipping the unnecessary Exif data copy. But there should still be some unknown reason in ExifInterface or these problematic devices to cause the issue.
Relnote: "Fixed JPEG image corruption issue if writing Exif location data on some Samsung Android 12 devices."
Bug: 263289024
Test: ImageCaptureTest
Change-Id: Ib70862aa6e654f06b9358e3f92bbb98c86cb9caf
M camera/camera-core/src/main/java/androidx/camera/core/impl/utils/Exif.java
M camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
ca...@gmail.com <ca...@gmail.com> #3
Thanks for reporting, i can use the example app to reproduce the problem. I saved the corrupted file and pulled it off the device and ran it through exiftool
, which complained (it doesn't complain as loudly about the source file, just some technically out-of-order tags):
$ exiftool -v2 editedFile.jpg
ExifToolVersion = 12.52
FileName = editedFile.jpg
Directory = .
FileSize = 433785
FileModifyDate = 1678109782
FileAccessDate = 1678109782
FileInodeChangeDate = 1678109782
FilePermissions = 33184
FileType = JPEG
FileTypeExtension = JPG
MIMEType = image/jpeg
Warning = [minor] Skipped unknown 617 bytes after JPEG APP1 segment
JPEG APP1 (46 bytes):
ExifByteOrder = II
Warning = Short directory size for IFD0 (missing 150 bytes)
| Warning = Bad IFD0 directory
Warning = [minor] Skipped unknown 917 bytes after JPEG NULL segment
JPEG SOI
JPEG APP0 (14 bytes):
+ [BinaryData directory, 9 bytes]
| JFIFVersion = 1 1
| - Tag 0x0000 (2 bytes, int8u[2])
| ResolutionUnit = 0
| - Tag 0x0002 (1 bytes, int8u[1])
| XResolution = 1
| - Tag 0x0003 (2 bytes, int16u[1])
| YResolution = 1
| - Tag 0x0005 (2 bytes, int16u[1])
| ThumbnailWidth = 0
| - Tag 0x0007 (1 bytes, int8u[1])
| ThumbnailHeight = 0
| - Tag 0x0008 (1 bytes, int8u[1])
JPEG DQT (65 bytes):
JPEG DQT (65 bytes):
JPEG SOF0 (15 bytes):
ImageWidth = 512
ImageHeight = 384
EncodingProcess = 0
BitsPerSample = 8
ColorComponents = 3
YCbCrSubSampling = 2 1
JPEG DHT (29 bytes):
JPEG DHT (179 bytes):
JPEG DHT (29 bytes):
JPEG DHT (179 bytes):
JPEG SOS
ki...@gmail.com <ki...@gmail.com> #4
The size of the JPEG APP1
segment looks wrong in the edited file (from exiftool
):
JPEG APP1 (46 bytes):
Compared to the original file:
JPEG APP1 (65279 bytes):
In binary this difference is visible in the 5th/6th byte of each file (from hexdump -C
). The first 4 bytes are a JPEG marker (0xFF
), a JPEG SOI (start of image, 0xd8
), another marker and an APP1 (0xe1
), followed by 2 bytes indicating the size of the APP1 segment.
Edited image:
ff d8 ff e1 00 30
Original image:
ff d8 ff e1 ff 01
Looking into why ExifInterface
is writing out the wrong APP1 size, I turned on
$ adb shell setprop log.tag.ExifInterface VERBOSE
And now it becomes a bit more obvious (this logging is from
saveJpegAttributes starting with (inputStream: java.io.BufferedInputStream@4fba9e8, outputStream: java.io.BufferedOutputStream@e5c3801)
index: 0, offsets: 8, tag count: 15, data sizes: 81, total size: 65584
65584
(0x10030
) is greater than the max 16-bit unsigned value (2^16 = 65536
), so when we ByteOrderedDataOutputStream.writeUnsignedShort
writeShort((short) val)
) truncates to the lower two bytes (0x0030
) - and this gets written out to the file and then breaks everything.
I think this likely only affects files that are very close to the 2^16
threshold, and the act of 'copying' all the EXIF attributes over probably reformats some of them just enough to take up slightly more bytes.
I'm not sure what is supposed to happen when a JPEG APP1 segment gets larger than 2^16
bytes, I need to dig a bit further into that part.
am...@google.com <am...@google.com> #5
Actually thinking about it more, it's not obvious to me why the APP1
segment of the original image is so large either - looking in exiftool
it doesn't seem to contain any particularly enormous fields, so I can't really see where 65kB comes from.
[Deleted User] <[Deleted User]> #6
re APP1
segment is so large, I missed the thumbnail the first time, which takes up a significant chunk (64kB):
+ [IFD1 directory with 15 entries]
| 0) ImageHeight = 384
| - Tag 0x0101 (4 bytes, int32u[1]):
| 0323: 80 01 00 00 [....]
| 1) Orientation = 6
| - Tag 0x0112 (2 bytes, int16u[1]):
| 032f: 06 00 [..]
| Warning = Tag ID 0x0103 Compression out of sequence in IFD1
| 2) Compression = 6
| - Tag 0x0103 (2 bytes, int16u[1]):
| 033b: 06 00 [..]
| 3) ThumbnailOffset = 1273
| - Tag 0x0201 (4 bytes, int32u[1]):
| 0347: f9 04 00 00 [....]
| 4) ThumbnailLength = 64000
| - Tag 0x0202 (4 bytes, int32u[1]):
| 0353: 00 fa 00 00 [....]
| Warning = Tag ID 0x010f Make out of sequence in IFD1
lo...@gmail.com <lo...@gmail.com> #7
APP1
:
Exif metadata are restricted in size to 64 kB in JPEG images because according to the specification this information must be contained within a single JPEG APP1 segment.
Charcoal: Do you know if CameraX is deciding how large to make the thumbnail in this case, or is this happening lower down in the device? Taking up 64,000 bytes doesn't leave much space for the rest of the Exif data.
Related thread I found (also complaining about a Samsung device, but from 2013...):
I experimented to see what exiftool
does now when the APP1
segment gets too large, and it looks like they just add a second APP1
segment and call it 'multi-segment EXIF` (I have no idea how well supported this is):
$ exiftool -UserComment="abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" b-263747161_original.jpg
Warning: [minor] File contains multi-segment EXIF - b-263747161_original.jpg
1 image files updated
$ exiftool -v3 b-263747161_original.jpg
ExifToolVersion = 12.52
FileName = b-263747161_original.jpg
Directory = .
FileSize = 434075
FileModifyDate = 1678121141
FileAccessDate = 1678121141
FileInodeChangeDate = 1678121141
FilePermissions = 33184
FileType = JPEG
FileTypeExtension = JPG
MIMEType = image/jpeg
JPEG APP1 (65533 bytes):
0006: 45 78 69 66 00 00 49 49 2a 00 08 00 00 00 0d 00 [Exif..II*.......]
0016: 00 01 04 00 01 00 00 00 a0 05 00 00 01 01 04 00 [................]
0026: 01 00 00 00 38 04 00 00 0e 01 02 00 01 00 00 00 [....8...........]
0036: 00 00 00 00 0f 01 02 00 08 00 00 00 aa 00 00 00 [................]
0046: 10 01 02 00 09 00 00 00 b2 00 00 00 12 01 03 00 [................]
0056: 01 00 00 00 06 00 00 00 1a 01 05 00 01 00 00 00 [................]
0066: bc 00 00 00 1b 01 05 00 01 00 00 00 c4 00 00 00 [................]
[snip 65421 bytes]
Warning = [minor] File contains multi-segment EXIF
JPEG APP1 (71 bytes):
10007: 45 78 69 66 00 00 00 00 00 00 00 00 00 00 00 00 [Exif............]
10017: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
10027: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
10037: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
10047: 00 00 00 00 00 00 00 [.......]
ExifByteOrder = II
Obviously the current ExifInterface
behaviour is very problematic (corrupting the image completely), but I'm not actually sure the best way to improve it. Some options I came up with:
- If the size of the
APP1
segment is larger than2^16
then just write the first2^16
bytes and hope. - Implement the same 'split segment' support that
exiftool
has. - Add logic to
ExifInterface
to resize a thumbnail to free up space in theAPP1
segment if it runs out (this seems too magical and could be very confusing for users). - Refuse to save a file in
ExifInterface.saveAttributes
if it results inAPP1
being larger than2^16
, either by silently not writing out (confusing) or throwing an exception (disruptive). - Add prioritisation of Exif tags, so when we run out of space we drop tags lowest-priority-first (very unlikely we can predict the 'correct' prioritisation such that users aren't annoyed/confused by the tags we decide to drop).
t....@gmail.com <t....@gmail.com> #8
Hi Ian,
Thanks for looking into the issue and provide the detailed analysis result.
About the question: Charcoal: Do you know if CameraX is deciding how large to make the thumbnail in this case, or is this happening lower down in the device? Taking up 64,000 bytes doesn't leave much space for the rest of the Exif data.
CameraX doesn't specify any thumbnail related capture request settings (
I'm curious about why this issue only happens when setting a FUSED location data but does not happen when setting a GPS location data? Does the FUSED location data in Exif actually occupy a little bit more size than GPS location data? So it causes the different result?
[Deleted User] <[Deleted User]> #9
I'm curious about why this issue only happens when setting a FUSED location data but does not happen when setting a GPS location data? Does the FUSED location data in Exif actually occupy a little bit more size than GPS location data? So it causes the different result?
Good question - I actually only got as far as playing with the image provided inside MyApplication-ExifLocationIssue.zip
, I didn't look at CameraXBasic-WriteFusedLocation.zip
, and as you noted in
ad...@gmail.com <ad...@gmail.com> #10
I've decided to go with option 4 from saveAttributes
in order to avoid creating a corrupted image. I've filed a new issue to track the possibility of adding multi-segment support in the future:
[Deleted User] <[Deleted User]> #11
Branch: androidx-main
commit c6bbc03f39f811ac546882880be46380cb31f5d5
Author: Ian Baker <ibaker@google.com>
Date: Tue Mar 07 11:09:08 2023
Throw an exception if trying to write a JPEG APP1 segment that's too large
jpeg_with_full_app_segment.jpg is an image taken with a Samsung A32
with a nearly-full APP1 segment due to a 64,000 byte thumbnail:
$ exiftool -v3 jpeg_with_exif_full_app1_segment.jpg
<snip>
JPEG APP1 (65072 bytes):
<snip>
| 5) ThumbnailLength = 64000
Bug: 263747161
Test: ./gradlew :exifinterface:exifinterface:connectedAndroidTest
Change-Id: Id421cd36d974ad4f29a2c6d6c730d99d8986d917
M exifinterface/exifinterface/build.gradle
M exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
A exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_exif_full_app1_segment.jpg
M exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
ch...@google.com <ch...@google.com> #12
Are you sure it's an issue with the APP1 segment being too large with the FUSED location? I was thinking perhaps there's some issue with the length calculation that's happening as we were able to fix these images by just detecting the start of the jpeg image data and re-writing the length field based on that detection.
[Deleted User] <[Deleted User]> #13
Are you sure it's an issue with the APP1 segment being too large with the FUSED location? I was thinking perhaps there's some issue with the length calculation that's happening as we were able to fix these images by just detecting the start of the jpeg image data and re-writing the length field based on that detection.
This fix is consistent with the current failure mode of ExifInterface
when the APP1
segment gets 'too large' (before my change linked above in 0x10030
bytes to write to the APP1 segment, and ExifInterface
does two things:
- When writing the length of the APP1 segment it truncates this to
0x0030
- It writes all those
0x10030
bytes out, and then it writes the rest of the image.
A JPEG parser (I assume) comes along and reads the length of the APP1
segment (0x30
) and skips that many bytes, then expects to find JPEG data - but it's actually in the middle of the Exif and so it fails.
If you manually find the start of the JPEG data then you obviate both of the problems - and the image is 'fixed'.
ja...@gmail.com <ja...@gmail.com> #14
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.exifinterface:exifinterface:1.3.7
ch...@google.com <ch...@google.com> #15
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.exifinterface:exifinterface:1.4.0-alpha01
[Deleted User] <[Deleted User]> #16
If you want to stuff this in a base class and know you're not setting any other behaviors on your appbarlayouts, you can put it in a base class in an overridden setLayoutParameters method
ch...@google.com <ch...@google.com> #17
@CoordinatorLayout.DefaultBehavior(FixAppBarLayoutBehavior.class)
public CustomAppBarLayout extends AppBarLayout {
// ....
}
That way, you do not have to mess around with your layouts or the LayoutParams.
am...@google.com <am...@google.com> #18
ar...@google.com <ar...@google.com> #19
az...@gmail.com <az...@gmail.com> #20
dg...@gmail.com <dg...@gmail.com> #21
[Deleted User] <[Deleted User]> #22
jo...@gmail.com <jo...@gmail.com> #23
dr...@gmail.com <dr...@gmail.com> #24
fa...@gmail.com <fa...@gmail.com> #25
ps...@gmail.com <ps...@gmail.com> #26
dr...@gmail.com <dr...@gmail.com> #27
dr...@gmail.com <dr...@gmail.com> #28
al...@google.com <al...@google.com> #29
am...@google.com <am...@google.com> #30
ps...@gmail.com <ps...@gmail.com> #31
de...@gmail.com <de...@gmail.com> #32
de...@gmail.com <de...@gmail.com> #33
pr...@gmail.com <pr...@gmail.com> #34
[Deleted User] <[Deleted User]> #35
ay...@gmail.com <ay...@gmail.com> #36
9g...@gmail.com <9g...@gmail.com> #37
[Deleted User] <[Deleted User]> #38
za...@gmail.com <za...@gmail.com> #39
sh...@gmail.com <sh...@gmail.com> #40
ja...@gmail.com <ja...@gmail.com> #41
de...@gmail.com <de...@gmail.com> #42
pe...@gmail.com <pe...@gmail.com> #43
Hoping to see a proper solution, currently this is my workaround until then. Not the best workaround, but does work for me:
Override
public void onScrollStateChanged(final int state)
{
super.onScrollStateChanged(state);
if (state == RecyclerView.SCROLL_STATE_SETTLING)
{
this.stopScroll();
}
}
pe...@gmail.com <pe...@gmail.com> #44
hu...@gmail.com <hu...@gmail.com> #45
[Deleted User] <[Deleted User]> #46
an...@gmail.com <an...@gmail.com> #47
[Deleted User] <[Deleted User]> #49
ha...@gmail.com <ha...@gmail.com> #50
ri...@gmail.com <ri...@gmail.com> #51
fa...@gmail.com <fa...@gmail.com> #52
de...@gmail.com <de...@gmail.com> #53
au...@google.com <au...@google.com> #54
sh...@google.com <sh...@google.com> #55
The issue is that after a fling event, the nested scrolling api has no way of informing the RecyclerView that a fling animation has reached the end of the scroll distance, and thus sometimes, the scroll animation is continuing in the background such that when the first tap occurs, it is interpreted as an interruption to the fling, which puts the RV back into its scrolling state as if the user started scrolling, and thus, the touch events don't propagate down to the child to cause a click to occur.
Some larger changes are needed to fix this (and at least one other) issue, and I'm in the midst of that work.
Sorry for the inconvenience.
za...@gmail.com <za...@gmail.com> #56
dr...@gmail.com <dr...@gmail.com> #57
May I ask in this version, fix this issue?
sh...@google.com <sh...@google.com> #58
sp...@gmail.com <sp...@gmail.com> #59
sh...@google.com <sh...@google.com> #60
When RecyclerView is flung, and it hits the bounds of it's scrollable distance and has a NestedScrollingParent, it continues to animate the fling so that the NestedScrollingParent might receive the events and scroll. RV has no way of knowing if any NestedScrollingParents have hit their bounds, and thus it continues the fling even if nothing is moving.
On top of that, when RV is flinging and is touched, it stops it's fling and prevents the touch event from causing a click, so that a touch meant to stop a fling doesn't cause a click.
Put those two things together and you get the perceived behavior that when an RV is flung, and stops, the next touch event doesn't click.
I'm pursuing a but can't make any promises as to when it will be available. Sorry for the inconvenience.
ha...@gmail.com <ha...@gmail.com> #61
pr...@gmail.com <pr...@gmail.com> #63
su...@gmail.com <su...@gmail.com> #64
sh...@google.com <sh...@google.com> #65
ny...@gmail.com <ny...@gmail.com> #66
fa...@gmail.com <fa...@gmail.com> #67
le...@gmail.com <le...@gmail.com> #68
pi...@gmail.com <pi...@gmail.com> #69
yi...@gmail.com <yi...@gmail.com> #70
de...@gmail.com <de...@gmail.com> #71
t....@gmail.com <t....@gmail.com> #72
cn...@gmail.com <cn...@gmail.com> #73
za...@gmail.com <za...@gmail.com> #74
da...@gmail.com <da...@gmail.com> #75
[Deleted User] <[Deleted User]> #76
ro...@gmail.com <ro...@gmail.com> #77
an...@googlemail.com <an...@googlemail.com> #78
sh...@google.com <sh...@google.com> #79
I am actively solving this issue. In fact, the CL that makes the necessary changes in RecyclerView has been submitted to aosp (as AndroidX is now developed there):
The fix is coming by way of an update to nested scrolling and thus need to be implemented across multiple classes. For example, for the issue to be fixed when a RecyclerView is in a CoordinatorLayout with the AppBarLayout.Behavior, implementation has to be done in each of those classes.
Unfortunately the rollout of these changes is also going to take some time, but do know that we are taking as seriously as we can.
fr...@gmail.com <fr...@gmail.com> #80
sh...@google.com <sh...@google.com> #81
sh...@gmail.com <sh...@gmail.com> #83
I've tried both java and xml ways but in vain.
Checked on emulator running on 5.0 and physical device running on 7.1.1
Any ideas ?
sh...@gmail.com <sh...@gmail.com> #84
ss...@gmail.com <ss...@gmail.com> #85
am...@gmail.com <am...@gmail.com> #87
am...@google.com <am...@google.com>
pa...@gmail.com <pa...@gmail.com> #88
Dear Google developers, can you please take it on priority?
we...@gmail.com <we...@gmail.com> #89
kp...@gmail.com <kp...@gmail.com> #90
I think it is related with NestedScrollingChild3 and NestedScrollingParent3 and since 1.1.0-alpha01 RecyclerView and CoordinatorLayout implements those.
sh...@google.com <sh...@google.com> #91
sh...@google.com <sh...@google.com> #92
androidx.core 1.1.0-alpha01
androidx.appcompat 1.1.0-alpha01
androidx.coordinatorlayout 1.1.0-alpha01
androidx.recyclerview 1.1.0-alpha01
androidx.swiperefreshlayout 1.1.0-alpha01
(and an upcoming release of the android material design library)
This issue should be fixed!
I'm closing it, but do let me know if it seems to still be happening!
pa...@outlook.com <pa...@outlook.com> #93
This is a BottomSheet with a NestedScrollView, it does not contain any RecyclerView:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="
xmlns:app="
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--Other views and tested with and without a Toolbar inside-->
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingTop="8dp">
<!--Enough views to create a scrollable area-->
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
In this scenario (with or without a Toolbar view in the AppBarLayout parent) whenever you scroll the NestedScrollView to the top or to the bottom and try to tap on any items inside the LinearLayout the first tap will always fail to initiate the expected action, forcing you to do a second tap since after the first one the taps start working. This happens whenever you scroll to the top or bottom of the NestedScrollView every single time, even after you tap for a second or n times.
I am using the library versions or most up to date (a couple were already updated since then) the Assignee listed in his last reply.
sh...@google.com <sh...@google.com> #94
There isn't a public bug that I'm aware of that is tracking that precise work, but know that your specific issue will be fixed in an upcoming release of the Android Material Design library.
pa...@outlook.com <pa...@outlook.com> #95
me...@google.com <me...@google.com> #96
yq...@gmail.com <yq...@gmail.com> #97
pv...@gmail.com <pv...@gmail.com> #98
recyclerview-v7:28.0.0
vz...@gmail.com <vz...@gmail.com> #99
sh...@google.com <sh...@google.com> #100
The AndroidX version of RecyclerView picks up where that version left off and is currently supported:
sh...@google.com <sh...@google.com> #101
sa...@gmail.com <sa...@gmail.com> #102
[Deleted User] <[Deleted User]> #103
[Deleted User] <[Deleted User]> #104
sh...@google.com <sh...@google.com> #105
te...@gmail.com <te...@gmail.com> #106
sh...@google.com <sh...@google.com> #107
ma...@gmail.com <ma...@gmail.com> #108
ma...@gmail.com <ma...@gmail.com> #109
but it's working fine if I double tap it quickly
sh...@google.com <sh...@google.com> #110
bo...@gmail.com <bo...@gmail.com> #111
Updated my AndroidX dependencies and fixed the issue with appbarlayout/bottomnav/recyclerview. Did not try out MotionLayout
Description
Version used: 26.0.2
Theme used: Theme.AppCompat.NoActionBar
Devices/Android versions reproduced on: AVD API 25
I just upgraded to API 26 and support library 26.0.2. But I found that my RecyclerView items is not clickable right after the scrolling. If you wait for a second, it will work. But if you click the item immediately, it won't. Even if the RecyclerView is not scrolling at all(e.g. has scrolled to the top).
When I downgraded to support library 25.4.0 everything goes fine again. The key point is that my RecyclerView is in a CoordinatorLayout and has a SCROLL_FLAG_SCROLL flag on my Toolbar of the AppBarLayout. If I don't use this flag, then this problem will disappear.
I've tried to add focusable="false" to the CoordinatorLayout but still had no luck.
Is there any way to disable this behavior? Because it's really annoying to click twice to trigger the click event.
I think the problem is the scrollState of the RecyclerView. When it's stopped scrolling, it's not changed to SCROLL_STATE_IDLE immediately. Looking into the source code of RecyclerView, I found there's a ViewFlinger controlling the scroll state. When I fling down to scroll to the top, it's not calling setScrollState(SCROLL_STATE_IDLE) immediately. Instead, it wait for a while to trigger this method. The more fast I fling, the more time I need to wait. It just like the RecyclerView is still scrolling in the background. Because the scroller.isFinished() doesn't return true right after the RecyclerView stop scrolling when it touched the top. Maybe it's a bug of the RecyclerView when it's in a CoordinatorLayout.
The attachment is a screen recording of this behavior.
<android.support.design.widget.CoordinatorLayout
android:id="@+id/coordinateLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/fragmentAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp"
android:background="@null">
<include
android:id="@+id/dynamicActionBarHolder"
layout="@layout/dynamic_action_bar"/>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/pullToRefreshMailRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="@+id/mailRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
</android.support.design.widget.CoordinatorLayout>
layout/dynamic_action_bar.xml
<FrameLayout xmlns:android="
xmlns:app="
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll"
android:clickable="true"
android:background="?theme.dynamicActionBarBackground">
<ImageButton
android:id="@+id/dynamicAcbMenuIcon"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:background="@drawable/article_explicit_button_background"
android:src="?theme.menuIcon"/>
<RelativeLayout
android:id="@+id/dynamicAcbTitleHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="?attr/actionBarSize"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:paddingEnd="5dp"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/dynamicAcbTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textSize="@dimen/action_bar_title_portrait_size"
android:textColor="?theme.listItemTitleColor"
android:ellipsize="end"
android:text="ActionBar"/>
<TextView
android:id="@+id/dynamicAcbSubtitle"
android:layout_below="@+id/dynamicAcbTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textSize="@dimen/action_bar_subtitle_portrait_size"
android:textColor="?theme.listItemTitleColor"
android:ellipsize="end"
android:text="If you say so"/>
</RelativeLayout>
</FrameLayout>