Skip to content
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

[wip] Image caching #343

Closed
wants to merge 21 commits into from
Closed

[wip] Image caching #343

wants to merge 21 commits into from

Conversation

Strubbl
Copy link
Contributor

@Strubbl Strubbl commented Nov 4, 2016

okay, i just implemented a stupid image downloader which has no queue and separate no thread yet.
This is a TODO. I just want to save work here and maybe get some feedback or (even better would be) support from somebody (looking at @di72nn or @tcitworld 😉 )

I am not working on this branch again before sunday evening or even monday, if somebody wants to contribute some code.

Copy link
Member

@tcitworld tcitworld left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work !
Just one remark : why are the image paths in html content replaced when the article is shown instead of when the content is fetched and the images saved ? I fear for latency when opening an article with multiple pics.

@@ -7,7 +7,7 @@ android {

defaultConfig {
applicationId "fr.gaulupeau.apps.InThePoche"
minSdkVersion 11
minSdkVersion 19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ? WRITE_EXTERNAL_STORAGE exists before.

Copy link
Contributor Author

@Strubbl Strubbl Nov 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so that we can use getExternalFilesDirs() to determine the real SD card and not only internal emulated external storage: https://developer.android.com/reference/android/content/Context.html#getExternalFilesDirs(java.lang.String)

if you have an other way to determine the REAL sd card path, i would be happy to see a commit :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea was to use getExternalFilesDirs() if we're on SDK >= 19 and default to internal emulated external storage in the other case.
According to Google Play, we have something like 5% of users below KitKat (and the F-Droid numbers are certainly higher), so I'll prefer to have an special case just for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, i understand that. But Kitkat is fairly old. Such user can still use the old app version, can't they?

I would really want to cache the images on my SD card, because they are consuming about 400 MB at my app. If we find a solution without getExternalFilesDirs to store on the real SD, let's just use that. getExternalFilesDir() (singular, without the s) does provide the /storage/emulated/ folder, which is the storage in the mobile and not the SD. That's really stupid behaviour by Android.

@Strubbl
Copy link
Contributor Author

Strubbl commented Nov 4, 2016

why are the image paths in html content replaced when the article is shown instead of when the content is fetched and the images saved ?

To keep the original article from wallabag. What happens if the user switches the setting back to not use cached images anymore? What happens if the user deletes the pictures on the SD card? Any user or app can do that. Then we have no way to retrieve the original pictures anymore.

I fear for latency when opening an article with multiple pics.

I think this is not a very costly operation. What do you mean by multiple pics? 100? 1000? So far i saw a performance improvement when i compare pure offline mode against online mode.

Just my 2 cents. I am open to any more concrete suggestions. There are for sure more open points to think about a lot.

@tcitworld
Copy link
Member

Good points. I'll see for myself about performance issues.

@di72nn
Copy link
Member

di72nn commented Nov 6, 2016

My idea was to use getExternalFilesDirs() if we're on SDK >= 19 and default to internal emulated external storage in the other case.

Done. Not sure about what getExternalFilesDir() returns, but we can deal with it later.

BTW, we can also add an option to move article DB to external storage.

@Strubbl
Copy link
Contributor Author

Strubbl commented Nov 6, 2016

BTW, we can also add an option to move article DB to external storage.

Yes, we could. But i would not use that option. That is not important to me, because i like the DB to be on the fast storage. SD Card can be slow (if you do not have class 10 or faster) and so the app might be slow aswell. Additionally, the DB is not that big, that i would need that on the SD Card to save internal space.

Another reason to not have the DB on SD is the case, where the user ejects SD card. In that case the app is not usable anymore. That case is cared for in this PR w.r.t. the images. If the image cannot be found on SD card, it is going to be loaded via web.

@di72nn
Copy link
Member

di72nn commented Nov 7, 2016

#347
I created a separate operation to BGService to fetch images. The operation can be interrupted - if any other operation is issued, the image fetching operation will finish as soon as possible and reschedule itself at the end of the IntentService queue.
The changes are the first draft, there are a lot of improvements to make. For example, if you interrupt the "image fetching" operation with "feed update" operation, the later will finish and schedule a new "image fetching" operation (so there would be two "image fetching" operations one after another, but the second one will finish fast anyway).

@di72nn
Copy link
Member

di72nn commented Nov 7, 2016

Also, the DB scheme change is there just to make the prototype work. I'm not yet sure how to store image cache info better.

@Strubbl
Copy link
Contributor Author

Strubbl commented Nov 7, 2016

I had the idea to create a separate DB table named download_image (columns: id, articleId, imageURL), where all images, which should be downloaded are written to. The table is filled while processing the RSS items (in the big loop for preparing the articles for the DB), so that it checks for images in the htmlContent of an article and checks if the file is already in cache, if not in cache it is added to that table.

After RSS feed sync the image fetching process should be triggered and the ImageFetch process works on the table. After successful or (in the first shot) failed download the row can be deleted from that table.

@tcitworld
Copy link
Member

Please rebase.

@di72nn
Copy link
Member

di72nn commented Dec 1, 2016

Rebased and force-pushed (to keep the PR). Also kept the old branch just in case https://github.com/wallabag/android-app/tree/image-caching-bak.

@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 7, 2016

I just got the following exception on current HEAD of this branch:

12-07 19:57:46.135 30061-31027/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: getCacheImagePath: articleId=2545
12-07 19:57:46.135 30061-31027/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: getCacheImagePath: localImageName=55358484277d43e96a6938f5757a1a06.jpg for URL http://www.golem.de/1612/124937-131465-i_rc.jpg
12-07 19:57:46.135 30061-31027/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: getCacheImagePath: localImagePath=/storage/3237-3031/Android/data/fr.gaulupeau.apps.InThePoche.debug/files/imagecache/2545/55358484277d43e96a6938f5757a1a06.jpg
12-07 19:57:46.135 30061-31027/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: downloadImageToCache: imageURL=http://www.golem.de/1612/124937-131465-i_rc.jpg destination=/storage/3237-3031/Android/data/fr.gaulupeau.apps.InThePoche.debug/files/imagecache/2545/55358484277d43e96a6938f5757a1a06.jpg
12-07 19:57:46.144 30061-31027/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: downloadImageToCache: file already exists, skipping
12-07 19:57:46.194 30061-31027/fr.gaulupeau.apps.InThePoche.debug E/SQLiteConnection: startPos 866 > actual rows 739
12-07 19:57:46.195 30061-31027/fr.gaulupeau.apps.InThePoche.debug E/CursorWindow: Failed to read row 34, column 0 from a CursorWindow which has 0 rows, 11 columns.
12-07 19:57:46.196 30061-31027/fr.gaulupeau.apps.InThePoche.debug D/EventProcessor: onFetchImagesFinishedEvent() started
                                                                                    
                                                                                    --------- beginning of crash
12-07 19:57:46.202 30061-31027/fr.gaulupeau.apps.InThePoche.debug E/AndroidRuntime: FATAL EXCEPTION: IntentService[BGService]
                                                                                    Process: fr.gaulupeau.apps.InThePoche.debug, PID: 30061
                                                                                    java.lang.IllegalStateException: Couldn't read row 34, col 0 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.
                                                                                        at android.database.CursorWindow.nativeGetLong(Native Method)
                                                                                        at android.database.CursorWindow.getLong(CursorWindow.java:511)
                                                                                        at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:75)
                                                                                        at org.greenrobot.greendao.AbstractDao.loadCurrent(AbstractDao.java:541)
                                                                                        at org.greenrobot.greendao.InternalQueryDaoAccess.loadCurrent(InternalQueryDaoAccess.java:33)
                                                                                        at org.greenrobot.greendao.query.LazyList.loadEntity(LazyList.java:268)
                                                                                        at org.greenrobot.greendao.query.LazyList.get(LazyList.java:255)
                                                                                        at org.greenrobot.greendao.query.LazyList$LazyIterator.next(LazyList.java:105)
                                                                                        at fr.gaulupeau.apps.Poche.service.BGService.fetchImages(BGService.java:493)
                                                                                        at fr.gaulupeau.apps.Poche.service.BGService.handle(BGService.java:187)
                                                                                        at fr.gaulupeau.apps.Poche.service.BGService.onHandleIntent(BGService.java:133)
                                                                                        at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:66)
                                                                                        at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                                        at android.os.Looper.loop(Looper.java:234)
                                                                                        at android.os.HandlerThread.run(HandlerThread.java:61)

@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 7, 2016

This IllegalStateException is reproducible even on a fresh new installation, where i manually deleted the cache image folder on the sd card manually to have a clean state.

It crashes with exact the same exception and after the same finished image being successfully download:

12-07 20:20:16.973 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/BGService: fetchImages() processing article 2545
12-07 20:20:16.974 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: cacheImages: articleId=2545 and is articleContent empty=false
12-07 20:20:16.974 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: cacheImages: extStoragePath=/storage/3237-3031/Android/data/fr.gaulupeau.apps.InThePoche.debug/files
12-07 20:20:16.975 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: findImageUrlsInHtml: found image URL http://www.golem.de/1612/124937-131465-i_rc.jpg
12-07 20:20:16.979 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: cacheImages: downloading http://www.golem.de/1612/124937-131465-i_rc.jpg
12-07 20:20:16.979 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: getCacheImagePath: articleId=2545
12-07 20:20:16.980 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: getCacheImagePath: localImageName=55358484277d43e96a6938f5757a1a06.jpg for URL http://www.golem.de/1612/124937-131465-i_rc.jpg
12-07 20:20:16.980 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: getCacheImagePath: localImagePath=/storage/3237-3031/Android/data/fr.gaulupeau.apps.InThePoche.debug/files/imagecache/2545/55358484277d43e96a6938f5757a1a06.jpg
12-07 20:20:16.980 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: downloadImageToCache: imageURL=http://www.golem.de/1612/124937-131465-i_rc.jpg destination=/storage/3237-3031/Android/data/fr.gaulupeau.apps.InThePoche.debug/files/imagecache/2545/55358484277d43e96a6938f5757a1a06.jpg
12-07 20:20:16.982 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: doesImageCacheDirExistElseCreate: articleImageCacheDirName=/storage/3237-3031/Android/data/fr.gaulupeau.apps.InThePoche.debug/files/imagecache/2545
12-07 20:20:17.005 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: doesImageCacheDirExistElseCreate: isDirCreated=true
12-07 20:20:17.077 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/ImageCacheUtils: downloadImageToCache: function finished for imageURL=http://www.golem.de/1612/124937-131465-i_rc.jpg destination=/storage/3237-3031/Android/data/fr.gaulupeau.apps.InThePoche.debug/files/imagecache/2545/55358484277d43e96a6938f5757a1a06.jpg
12-07 20:20:17.123 8437-11190/fr.gaulupeau.apps.InThePoche.debug E/SQLiteConnection: startPos 866 > actual rows 739
12-07 20:20:17.123 8437-11190/fr.gaulupeau.apps.InThePoche.debug E/CursorWindow: Failed to read row 34, column 0 from a CursorWindow which has 0 rows, 11 columns.
12-07 20:20:17.129 8437-11190/fr.gaulupeau.apps.InThePoche.debug D/EventProcessor: onFetchImagesFinishedEvent() started
                                                                                   
                                                                                   --------- beginning of crash
12-07 20:20:17.133 8437-11190/fr.gaulupeau.apps.InThePoche.debug E/AndroidRuntime: FATAL EXCEPTION: IntentService[BGService]
                                                                                   Process: fr.gaulupeau.apps.InThePoche.debug, PID: 8437
                                                                                   java.lang.IllegalStateException: Couldn't read row 34, col 0 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.
                                                                                       at android.database.CursorWindow.nativeGetLong(Native Method)
                                                                                       at android.database.CursorWindow.getLong(CursorWindow.java:511)
                                                                                       at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:75)
                                                                                       at org.greenrobot.greendao.AbstractDao.loadCurrent(AbstractDao.java:541)
                                                                                       at org.greenrobot.greendao.InternalQueryDaoAccess.loadCurrent(InternalQueryDaoAccess.java:33)
                                                                                       at org.greenrobot.greendao.query.LazyList.loadEntity(LazyList.java:268)
                                                                                       at org.greenrobot.greendao.query.LazyList.get(LazyList.java:255)
                                                                                       at org.greenrobot.greendao.query.LazyList$LazyIterator.next(LazyList.java:105)
                                                                                       at fr.gaulupeau.apps.Poche.service.BGService.fetchImages(BGService.java:494)
                                                                                       at fr.gaulupeau.apps.Poche.service.BGService.handle(BGService.java:187)
                                                                                       at fr.gaulupeau.apps.Poche.service.BGService.onHandleIntent(BGService.java:133)
                                                                                       at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:66)
                                                                                       at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                                       at android.os.Looper.loop(Looper.java:234)
                                                                                       at android.os.HandlerThread.run(HandlerThread.java:61)
12-07 20:20:17.182 8437-8437/fr.gaulupeau.apps.InThePoche.debug D/ArticlesListFragment: Fragment 0 onPause()

BTW, this article 2545 is the last in the LazyList.

@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 7, 2016

Maybe we experience a bug in org.greenrobot.greendao.query.LazyList here. I just tried using normal list() with a List<Article> and this works as expected without any exception.

see attached patch

0001-use-List-instead-of-LazyList-when-querying-for-artic.patch.txt

@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 7, 2016

just also saw some timeouts being catched, where i am going to add some more information:

12-07 21:31:25.681 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err: java.net.SocketTimeoutException
12-07 21:31:25.681 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at java.net.PlainSocketImpl.read(PlainSocketImpl.java:484)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at java.net.PlainSocketImpl.access$000(PlainSocketImpl.java:37)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at java.net.PlainSocketImpl$PlainSocketInputStream.read(PlainSocketImpl.java:237)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at okio.Okio$2.read(Okio.java:138)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at okio.AsyncTimeout$2.read(AsyncTimeout.java:238)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at okio.RealBufferedSource.read(RealBufferedSource.java:45)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at okhttp3.internal.http.Http1xStream$FixedLengthSource.read(Http1xStream.java:377)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at okio.RealBufferedSource.read(RealBufferedSource.java:45)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at okio.RealBufferedSink.writeAll(RealBufferedSink.java:97)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at fr.gaulupeau.apps.Poche.network.ImageCacheUtils.downloadImageToCache(ImageCacheUtils.java:150)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at fr.gaulupeau.apps.Poche.network.ImageCacheUtils.cacheImages(ImageCacheUtils.java:59)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at fr.gaulupeau.apps.Poche.service.BGService.fetchImages(BGService.java:502)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at fr.gaulupeau.apps.Poche.service.BGService.handle(BGService.java:186)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at fr.gaulupeau.apps.Poche.service.BGService.onHandleIntent(BGService.java:132)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:66)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at android.os.Handler.dispatchMessage(Handler.java:102)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at android.os.Looper.loop(Looper.java:234)
12-07 21:31:25.682 7404-7755/fr.gaulupeau.apps.InThePoche.debug W/System.err:     at android.os.HandlerThread.run(HandlerThread.java:61)

@di72nn
Copy link
Member

di72nn commented Dec 8, 2016

I don't get the exception. Did you change anything else (dependencies' versions, etc.)?

@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 8, 2016

No, only gradle, the PR which just got merged.

@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 19, 2016

I completely wiped my app and the content on the SD card and build a clean debug version of this branch here. Unfortunately i can still reproduce the crash with the LazyList.

So i installed the app, configured it with my server, and the initial sync was done automatically. Then i changed the setting to enable image caching and pushed the button to sync. After a while (i have approx. 2000 articles in wallabag), i get the crash of the app. Even after restarting the app, it starts automatically to sync again and of crashes again. Restarting it for the 2nd time it still starts to sync and crashes. And after the third time, it just starts the app without automatically syncing. If it push the sync button again, i can reproduce the behaviour and the loop of crashes repeats again.

The only change i have made to this branch is:

$ git diff
diff --git a/build.gradle b/build.gradle
index fbf8604..8417239 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ buildscript {
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.2.2'
+        classpath 'com.android.tools.build:gradle:2.2.3'
         classpath 'org.greenrobot:greendao-gradle-plugin:3.2.0'
     }
 }

Can we please apply my patch and get this PR merged?
Except of this bug i see no further show stopper from merging this PR. If we get it into the next release, we can gather some feedback of the early adopters. Also we can try to use the lazylist again in a fresh new branch, after your dependency PR #379 has been merged.

@Strubbl Strubbl changed the title [WIP] Image caching Image caching Dec 19, 2016
@Strubbl Strubbl changed the title Image caching [wip] Image caching Dec 20, 2016
@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 20, 2016

Open points before merging from @di72nn:

  • review the rest of the changes in this PR
  • some not yet defined actions of housekeeping

@Strubbl
Copy link
Contributor Author

Strubbl commented Dec 28, 2016

due to having #394 we can close this PR?

@tcitworld tcitworld closed this Dec 28, 2016
@tcitworld tcitworld deleted the image-caching branch December 28, 2016 17:23
@di72nn
Copy link
Member

di72nn commented Dec 28, 2016

The PR may be obsolete, but I restored the image-caching branch just in case.

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

Successfully merging this pull request may close these issues.

3 participants