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

Allow "file:" and "[[...]]" links, display images in notes #389

Merged
merged 2 commits into from
Oct 30, 2018

Conversation

aancel
Copy link
Contributor

@aancel aancel commented Sep 23, 2018

Hello,
Following the work on #382, I open a PR with new propositions.
I reworked the code a bit to better integrate it with the previous code, and notably to handle [[...]] for file links (still linked to #360) in addition to the previous file: scheme.
I also added an ImageSpan to display images in notes (rescaled to fit width, if larger than the window width), as a proposition to implement #147.

As before, this PR has been tested with API 16 (Nexus 5), API 23 (Pixel 2) & API 27 (Pixel 2 XL)

A small note sample, I used to test this:

[[Download/test.jpg]]

file:Download/test2.png

[[Download/test3.png][Test]]

Right now, named links are not replaced with an ImageSpan.
Furthemore regarding to the application performance, maybe adding a setting to toggle off image display could be a good idea. What do you think about this ?

@nevenz
Copy link
Member

nevenz commented Oct 1, 2018

I also added an ImageSpan to display images in notes

Cool!

rescaled to fit width, if larger than the window width

What's the expected size of the displayed image when it's too wide? I'm seeing half a screen width in portrait mode and the same size in landscape. Should it depend on orientation?

file:Download/test2.png

I don't think this is displayed in Org mode. Only images in square bracket links. I think we should do the same.

Right now, named links are not replaced with an ImageSpan.

Yeah, we'll want these displayed too.

Furthemore regarding to the application performance, maybe adding a setting to toggle off image display could be a good idea. What do you think about this ?

Sounds good. Perhaps even keeping the feature off by default, though I'm not sure about that.

I'll leave few comments in the code too.

Thanks!

if (file.exists()
&& (file.extension.toLowerCase() == "png"
|| file.extension.toLowerCase() == "jpg"
|| file.extension.toLowerCase() == "bmp")
Copy link
Member

Choose a reason for hiding this comment

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

toLowerCase can be called multiple times here. I'd suggest moving those to a new method (isImageExtension or whatever). Also, I'd call that before file.exists(), as I think it might be faster. I'd even use path instead, to avoid creating File. This is one of the places which we should optimize if we can, as it can be called quite a few times fast (when scrolling for example).

Are those 3 extensions the only ones supported for Drawable BTW? What about jpeg for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've update this code, and the supported extensions via the ImageUtils class.
I did not find a portable way to know the supported image formats, so I used the ones described in the android documentation: https://developer.android.com/guide/topics/media/media-formats

// Read the image in a Drawable
val currentActivity = App.getCurrentActivity()
if(currentActivity != null) {
val inputStream = currentActivity.contentResolver.openInputStream(contentUri)
Copy link
Member

Choose a reason for hiding this comment

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

Does this need closing? Probably not, but it might be cleaner if it's done?

} else {
cs = configureFileLink(ssb, link, m.start(), m.end())
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This splitting and checking for number of elements is a bit hard to read. I was wondering what's == 1 for, but that's for links such as [[file.jpg]] ? So it's basically a link with file scheme or no scheme? Maybe using something like fun fileLinkPath(link: String): String? would be easier to read, also avoiding this last check too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I remember well, the == 1 is because when splitting a string without the specified delimiter, the method will return an Array of size 1 with the original string, so it should handle the case of not having a file: scheme.
Indeed I'll try to simplify this !

@nevenz
Copy link
Member

nevenz commented Oct 1, 2018

Few more things...

Displayed images are now clickable too. Do we want that behavior? I don't really have a preference.

Images can now be included and displayed in note title too. I don't think we should support that, even though it seems kinda cool. It's messing up the layout which could be tricky to fix. And even when fixed, I think notes would be harder to read with variable height titles (multi-line or not). Then again, Org mode support that.

Links like [[foo]] are now linkified, leaving square brackets visible. Is there a clean and fast way not to do that unless it's a path to existing image? Org mode would look for a heading name, which is not supported in Orgzly (yet). Probably not a big deal, just a bit confusing (to be at least).

Code style needs some fixing. Space after if is missing in few places. And for consistency, else would have to be moved to the same line as }. Though I'm just nitpicking now.

@nevenz nevenz added this to the v1.6.10 milestone Oct 1, 2018
@aancel
Copy link
Contributor Author

aancel commented Oct 1, 2018

Ok, I've updated the code a bit, but there are still missing points:

What's the expected size of the displayed image when it's too wide? I'm seeing half a screen width in portrait mode and the same size in landscape. Should it depend on orientation?

Here I am not very happy with the use of currentActivity.window.decorView.width. This variable indicates the current width of the window (e.g. 1920 or 1080 for a 1080p device).
Normally, if the image is smaller than the current width (which should work in both portrait and landscape mode), it doesn't get resized (which can end up with very small pictures ...), but if it is larger than the current width, it is resized to match the max width, so it is viewable.
I did not encouter issues with my test images however ... I'll give it another try.
If you have ideas on a better way to get a max width, I'm open to suggestions !

file:Download/test2.png

I don't think this is displayed in Org mode. Only images in square bracket links. I think we should do the same.

I usually don't display my images in org-mode, so it did not bother me to have it displayed, but I can disable that if you prefer.

Right now, named links are not replaced with an ImageSpan.

Yeah, we'll want these displayed too.

I haven't fixed this yet. It seems to behave differently than the unnamed links, and I was not able to find why as of now.

Furthemore regarding to the application performance, maybe adding a setting to toggle off image display could be a good idea. What do you think about this ?

Sounds good. Perhaps even keeping the feature off by default, though I'm not sure about that.

I've added a boolean in the Config object, this should make it easier to set off.

Few more things...

Displayed images are now clickable too. Do we want that behavior? I don't really have a preference.

I think this is a good idea to keep this, in case the user wants to display it in an image viewer for zooming (for small images) or modifications, in the same way you can open external links in org-mode.

Images can now be included and displayed in note title too. I don't think we should support that, even though it seems kinda cool. It's messing up the layout which could be tricky to fix. And even when fixed, I think notes would be harder to read with variable height titles (multi-line or not). Then again, Org mode support that.

This should be fixed with the additional config parameter I added. I also took the liberty to remove it from the reminders, as It would probably also mess with the layout in narrow spaces.

Links like [[foo]] are now linkified, leaving square brackets visible. Is there a clean and fast way not to do that unless it's a path to existing image? Org mode would look for a heading name, which is not supported in Orgzly (yet). Probably not a big deal, just a bit confusing (to be at least).

I've modified the function generating a ClickableSpan, so it does not create a link when the link is not valid for a file. It kinds of implement indicating that the file is invalid, as we discussed in the previous PR.
However this causes a File instance and file.exists() check to be done a bit earlier, which is something that you wanted to avoid in a previous comment. So if you prefer the previous way, I'll revert back to this.

Code style needs some fixing. Space after if is missing in few places. And for consistency, else would have to be moved to the same line as }. Though I'm just nitpicking now.

The code style should be fixed hopefully !

@nevenz
Copy link
Member

nevenz commented Oct 2, 2018

I did not encouter issues with my test images however ... I'll give it another try.

I've been using 2560x1600 JPEG and seeing:

  • decorWidth: 1440
  • drawableHeight: 457
  • drawableWidth: 731

for whatever reason.

If you have ideas on a better way to get a max width, I'm open to suggestions !

It's tricky because of possible indentation too. Higher-level notes will have less space available. Also, there could be text before and after the image on the same line, though that will wrap.

This is what I'm seeing:

orgzly-photos

Which looks fine, but it's unexpected. I thought image would take more space. I haven't tested it with different density devices.

Right now, named links are not replaced with an ImageSpan.

Yeah, we'll want these displayed too.

I haven't fixed this yet. It seems to behave differently than the unnamed links, and I was not able to find why as of now.

Perhaps adding some tests to OrgFormatterTest might help. I don't see anything wrong at first glance.

However this causes a File instance and file.exists() check to be done a bit earlier, which is something that you wanted to avoid in a previous comment. So if you prefer the previous way, I'll revert back to this.

I'm more concerned about the speed of loading images now. Drawable.createFromStream is very slow. When you scroll in a notebook with a lot of images (like the one in the screenshot), it's very choppy. And that's with only two images being loaded over and over again. Perhaps they can be loaded in background at least. Maybe even caching drawables. We'll have to look into how that's done.

BTW, I noticed [[Download/logo.jpg/]] is creating a link leaving square brackets visible. Download/logo.jpg does exist (and it's opened on tap, which is also weird), but this is a link to Download/logo.jpg/, so it shouldn't be linkified. This is another good case to check in tests.

@aancel
Copy link
Contributor Author

aancel commented Oct 2, 2018

Indeed, I haven't tested with notes on other levels, so the pebble/butterfly image should be exactly the same size on higher level notes.
However I don't think that it really makes sense to further reduce the size of the images as the note level increases, in the end you would have really small images forced to fit in the view, and images that would disappear because they would be outside of the visible area.
So I think it might be better to have a reasonable resizing and having the user being conscient about it.
What do you think about this ?

Regarding to the loading speed of the images, there seem to be some LruCache available on Android, see https://developer.android.com/topic/performance/graphics/cache-bitmap.
I think that it might be a good idea to use this.
There are also third party libraries that handle image loading in the background, like Picasso or Glide. What do you think about using one of those ? (I'm asking because you might want to avoid including external dependencies).
Combined with the LruCache, I think it can help.

I'll try to look at your other remarks when I have more time !

@nevenz
Copy link
Member

nevenz commented Oct 3, 2018

However I don't think that it really makes sense to further reduce the size of the images as the note level increases, in the end you would have really small images forced to fit in the view, and images that would disappear because they would be outside of the visible area.
So I think it might be better to have a reasonable resizing and having the user being conscient about it.

Yes, I agree. It's just that I don't understand why this huge image doesn't get to fill the whole width. So I'm worried that there could be issues on some other devices. But if we check few more different sizes and densities and it looks OK, I'm fine with that.

Regarding to the loading speed of the images, there seem to be some LruCache available on Android, see https://developer.android.com/topic/performance/graphics/cache-bitmap.
I think that it might be a good idea to use this.
There are also third party libraries that handle image loading in the background, like Picasso or Glide. What do you think about using one of those ? (I'm asking because you might want to avoid including external dependencies).
Combined with the LruCache, I think it can help.

Notebook in the screenshot I posted might not be a good example of a typical usage. Most likely the images will be all different. I can easily see users having bunch of them in many notes. So, caching might not be as important as loading images in the background. Not that we shouldn't try to use cache if it's not too hard.

If Glide can help in any way, great. We shouldn't write more code then we need to, I don't mind external dependency unless it's for something trivial.

@aancel
Copy link
Contributor Author

aancel commented Oct 7, 2018

I've pushed another update, implementing image asynchronous loading and caching, which should hopefully improve performance. This was harder that I thought at the beginning ! Especially with the use of ImageSpan (It seems easier with classical views)
I've also added preferences to enable the display of images and specify whether the user wants the images to scale or to have a fixed size.

I ended up not using glide, as it was not that easy to use, and they seem to be changing APIs right now, with the deprecation of some objects. I used an AsyncTask and the LruCache.
I've also passed the TextViewWithMarkup to OrgFormatter. I'm not really a fan of this, but it gives more flexibility on the scaling of images.

One issue that I have now is with the preferences. When toggling the new ones I added, the settings don't get applied until a restart of the application. If you have any clue of what I might have done wrong, please tell me, so I can fix this.

I also reworked the code with the scheme and file: handling and splitting.

I've also tested the app with images similar in size of the big ones you use, loading and scaling is much better for me now.

@nevenz
Copy link
Member

nevenz commented Oct 8, 2018

I don't see any images now, just linkified [[Download/logo.jpg]]. Image is displayed when clicked on. I've tried toggling the option (including leaving the app), but also just replacing AppPreferences call to true.

Wouldn't this also have issues due to view recycling? And also potentially start bunch of AsyncTasks when scrolling fast?

I don't think this'll be that easy to get it right. We should definitely check how it's being done in some apps out there. It's a common thing to do, though I agree, perhaps not so much with ImageSpan. But it should be similar.

I've also passed the TextViewWithMarkup to OrgFormatter. I'm not really a fan of this, but it gives more flexibility on the scaling of images.

I think it's OK, but then I'd remove OrgFormatter.parse from TextViewWithMarkup.setText and do it the other way around - call new OrgFormatter.setText(TextViewWithMarkup, String) on the view.

BTW, I don't see anything wrong with the preference - when I install app for the first time, it's set to true.

@aancel
Copy link
Contributor Author

aancel commented Oct 8, 2018

I don't see any images now, just linkified [[Download/logo.jpg]]. Image is displayed when clicked on. I've tried toggling the option (including leaving the app), but also just replacing AppPreferences call to true.

Do you still use the same platforms as in the previous PRs ? Do you have the same issue on all the platforms ?
I've tested with Pixel 2 API 23 and Pixel 2 XL API 27, and it seems to be working fine. I'm having some issues with the Nexus 5 API 16 however. I'll start working on this trying to see if the issue behind is related to what you encounter.

Wouldn't this also have issues due to view recycling? And also potentially start bunch of AsyncTasks when scrolling fast?

While scrolling fast I didn't encounter any issue on my side.

I don't think this'll be that easy to get it right. We should definitely check how it's being done in some apps out there. It's a common thing to do, though I agree, perhaps not so much with ImageSpan. But it should be similar.

I'll look for similar stuff, but yeah it's less common with ImageSpan that classical views.

I think it's OK, but then I'd remove OrgFormatter.parse from TextViewWithMarkup.setText and do it the other way around - call new OrgFormatter.setText(TextViewWithMarkup, String) on the view.

Ok, I'll go this way, once I sorted out the loading issues.

Also regarding your previous comment:

BTW, I noticed [[Download/logo.jpg/]] is creating a link leaving square brackets visible. Download/logo.jpg does exist (and it's opened on tap, which is also weird), but this is a link to Download/logo.jpg/, so it shouldn't be linkified. This is another good case to check in tests.

I can also reproduce this, but it seems that File considers that Download/logo.jpg/ is a file, when it should be considered as a folder.

@nevenz
Copy link
Member

nevenz commented Oct 8, 2018

Do you still use the same platforms as in the previous PRs ? Do you have the same issue on all the platforms ?

Yes, I also tried to checkout the previous commit and I do see the images there. Then going back to latest one, I don't.

I'm having some issues with the Nexus 5 API 16 however.

What are you seeing there? I haven't tried API 16.

While scrolling fast I didn't encounter any issue on my side.

I suspect there could be updates to the wrong views, but we'll see.

I can also reproduce this, but it seems that File considers that Download/logo.jpg/ is a file, when it should be considered as a folder.

But this shouldn't pass the check for extension though?

@aancel
Copy link
Contributor Author

aancel commented Oct 8, 2018

Yes, I also tried to checkout the previous commit and I do see the images there. Then going back to latest one, I don't.

It's really very strange, because I've tested it on a fresh image with a Nexus 6P API 25 and it works as expected... I don't get where the issue is.
Could you try to put some log in the AsyncImageLoader.java class, notably in the doInBackground function to see if the app goes here and whether it loads image from the cache or not ?
It will at least tell us if the threads are launched and maybe if it is a rendering issue. If not, then the issue should probably be in the tests of OrgFormatter.kt.

What are you seeing there? I haven't tried API 16.

The issue here was with a bad test that broke the image display when the image is at the very end of the note and that the ]] closes the note.

I suspect there could be updates to the wrong views, but we'll see.

There is an additional problem with views: When the storage permission is not granted at the start of the app, then launch the app, try to click on a link to make the permission snackbar appear, grant the permission, then back to the app, the image are not immediately refreshed. I suspect that the view are not refreshed when resuming the app.

But this shouldn't pass the check for extension though?

The extension check is only performed when we want to display an image with an ImageSpan.
Classical link (via ClickableSpan) can be any file like a pdf, txt or an image and are thus linkified.

I don't know if I will have much time this evening, but I'll do my best to sort all those issues !

@nevenz
Copy link
Member

nevenz commented Oct 8, 2018

What are you seeing there? I haven't tried API 16.
The issue here was with a bad test that broke the image display when the image is at the very end of the note and that the ]] closes the note.

Ah, it looks like this is it. All my notes (well, only 3 unique, repeated bunch of times) are like that. Images are displayed when I add a space after ]].

Few more things I noticed...

Images stop being displayed when I:

  • Enter Settings
  • Press Back
  • Press Back (to return to notebook list)
  • Enter the same notebook with images again

When scrolling up slowly, there is sudden jump as note comes into view, I assume due to image being inserted and note now occupying more space.

With scaling option on, images occupy maximum width, even if there is some text after them, which then wraps. Might be OK, I'm not sure.

With scaling option off, aspect ratio doesn't seem to be kept.

When toggling above option, only images from first few notes are displayed. When I keep scrolling down, there are no images. When I go back to the top, few images previously displayed are gone.

@aancel
Copy link
Contributor Author

aancel commented Oct 8, 2018

Ah, it looks like this is it. All my notes (well, only 3 unique, repeated bunch of times) are like that. Images are displayed when I add a space after ]].

This should be fixed in the latest commit !

With scaling option off, aspect ratio doesn't seem to be kept.

I also updated this, so only the width is fixed. The height is computed to keep the aspect ratio.

Images stop being displayed when I:

Enter Settings
Press Back
Press Back (to return to notebook list)
Enter the same notebook with images again

I have a similar issue, when I toggle one of the settings ...
Even worse, when I toggle one of the setting, and go back, then back to the note (There the images are not displayed any longer). If I go back to the notebook list, I cannot go back into the notebook I exited from ... I have to relaunch the application to access it again. That's why I think there might have been an issue with the settings, or maybe it would be related to the views not being refreshed.
Would you have an idea where to look for ? Somewhere in the onResume of MainActivity maybe ?

When scrolling up slowly, there is sudden jump as note comes into view, I assume due to image being inserted and note now occupying more space.

Yes, I agree it is a bit disturbing, and I don't really know how to compensate for that.

With scaling option on, images occupy maximum width, even if there is some text after them, which then wraps. Might be OK, I'm not sure.

This is indeed debatable, and that's also why I put a scaling parameter to give the choice to the user.
I suppose that the people who want full width images won't wrap text around.
(Feedback from users about use-cases would really be valuable here)

@nevenz
Copy link
Member

nevenz commented Oct 10, 2018

I also updated this, so only the width is fixed. The height is computed to keep the aspect ratio.

Cool.

In both cases (scaling or not) - I don't think image should be scaled up. Or at least there should be another option to control that?

Also, current hardcoded 256 fixed image size should probably be configurable too.

Would you have an idea where to look for ? Somewhere in the onResume of MainActivity maybe ?

Current (tracked) activity is lost after returning from Settings:

10-10 09:53:48.600   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: onActivityResumed: WIP com.orgzly.android.ui.MainActivity@8cf807
10-10 09:53:48.600   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: registerActivity: WIP com.orgzly.android.ui.MainActivity@8cf807
10-10 09:53:48.850   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: onActivityStopped: WIP com.orgzly.android.ui.settings.SettingsActivity@303ac9c
10-10 09:53:48.850   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: registerActivity: WIP null

registerActivity should only be tracking CommonActivity. A simple:

    @Override
    public void onActivityResumed(Activity activity) {
        if (activity instanceof CommonActivity) {
            App.setCurrentActivity((CommonActivity) activity);
        }
    }

    @Override
    public void onActivityPaused(Activity activity) {
        if (activity instanceof CommonActivity) {
            App.setCurrentActivity(null);
        }
    }

should be enough, removing everything from other methods.

I don't know if this is also the reason for the issue you seeing - not being able to return to the notebook.

Yes, I agree it is a bit disturbing, and I don't really know how to compensate for that.

Without knowing image dimension in advance, it's probably not possible. User experience could be improved though:

  • Move cache check out from AsyncImageLoader. When Bitmap is found in cache, display the image immediately. Only helps if image is cached of course. But I think this is the Right Thing to do anyway - AsyncImageLoader should only be needed if we have to read the stream.
  • Display a placeholder image before calling AsyncImageLoader. This should decrease the difference in height when the real image is displayed.
  • Animate item expansion

@nevenz
Copy link
Member

nevenz commented Oct 10, 2018

I've fixed snackbar issue in master, hence the conflict, though it should be easy to resolve.

I've also created OrgFormatterLinkTest so we can list all the cases - should the link be parsed or not, clickable or not etc.

This test is failing right now, there are two fixes needed in OrgFormatter, but I didn't want to edit it more. We should have split this PR - one for rework, one for images but oh well.

@aancel
Copy link
Contributor Author

aancel commented Oct 10, 2018

Ok, thanks for the update !
Should I rebase on or merge the latest master to continue working on this ?

Yes indeed I agree for the split.

@nevenz
Copy link
Member

nevenz commented Oct 10, 2018

Should I rebase on or merge the latest master to continue working on this ?

I prefer rebase, but whatever is easier for you. Thanks!

@aancel
Copy link
Contributor Author

aancel commented Oct 10, 2018

In both cases (scaling or not) - I don't think image should be scaled up. Or at least there should be another option to control that?

With scaling on, images should never be scaled up. They are either kept to their original size if they fit within the view width or scaled down to fit the current view width.
With scaling off, the images might be scaled up if the user desires indeed.
But maybe, here you are referring to the multiplication of the drawable size by metrics.scaledDensity ? This was done to recover the original image size, as apparently android seem to be scaling down the images with the density.
This indeed could be configurable.
Did you mean anything else ?

Also, current hardcoded 256 fixed image size should probably be configurable too.

I agree, this was mainly as a first test, I'll add a preference to set this.

Current (tracked) activity is lost after returning from Settings:

10-10 09:53:48.600   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: onActivityResumed: WIP com.orgzly.android.ui.MainActivity@8cf807
10-10 09:53:48.600   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: registerActivity: WIP com.orgzly.android.ui.MainActivity@8cf807
10-10 09:53:48.850   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: onActivityStopped: WIP com.orgzly.android.ui.settings.SettingsActivity@303ac9c
10-10 09:53:48.850   665   665 D com.orgzly.android.ui.CommonActivityLifecycleCallbacks: 2#main: registerActivity: WIP null

registerActivity should only be tracking CommonActivity. A simple:

    @Override
    public void onActivityResumed(Activity activity) {
        if (activity instanceof CommonActivity) {
            App.setCurrentActivity((CommonActivity) activity);
        }
    }

    @Override
    public void onActivityPaused(Activity activity) {
        if (activity instanceof CommonActivity) {
            App.setCurrentActivity(null);
        }
    }

should be enough, removing everything from other methods.

Indeed, I remove the else part of the registerActivity test to implement what you propose

I don't know if this is also the reason for the issue you seeing - not being able to return to the notebook.

This doesn't seem to be fixed. I'll investigate this.

Yes, I agree it is a bit disturbing, and I don't really know how to compensate for that.

Without knowing image dimension in advance, it's probably not possible. User experience could be improved though:

* Move cache check out from `AsyncImageLoader`. When `Bitmap` is found in cache, display the image immediately. Only helps if image is cached of course. But I think this is the Right Thing to do anyway - `AsyncImageLoader` should only be needed if we have to read the stream.

* Display a placeholder image before calling `AsyncImageLoader`. This should decrease the difference in height when the real image is displayed.

* Animate item expansion

Great, I'll see what I can do on this side, although I'm just not sure that item animation is possible.

I prefer rebase, but whatever is easier for you. Thanks!

I can go for a rebase. I prefer asking because some people don't like forced pushes !

@nevenz
Copy link
Member

nevenz commented Oct 10, 2018

With scaling on, images should never be scaled up.
Did you mean anything else ?

No, you're right. It due to different issue I encountered.

I pushed a new tiny image to the emulator by overwriting the old one, left the list (or app), returned to it and went back to the book. I saw the same image, not the new one. It's due to caching by name probably and app didn't terminate.

I don't think we need to keep the cache during entire app run. It's enough to create it when user enters the notebook (or search results). NoteListFragment might be a good place to do it as it's used by both notebook and search results.

But even then, we'll need a way to invalidate the entires. And/or store them by something other then just file name. I'd much rather have cache misses here, then stale data. Cache is much less important now with async loading. I don't consider this a blocker for this PR though. Just moving the cache from App will do for now. (I'd also suggest wrapping that whole code in a new class.)

I don't know if this is also the reason for the issue you seeing - not being able to return to the notebook.
This doesn't seem to be fixed. I'll investigate this.

Is it #348 or similar perhaps? That would be helpful, as I wasn't able to reproduce that one.

Great, I'll see what I can do on this side, although I'm just not sure that item animation is possible.

There's probably a way (worst case generating two item views and animating the replacement, or something like that). But definitely not a blocker for this PR, we can do that later.

@aancel aancel force-pushed the update-external-file-image branch from ef519d0 to 977b55e Compare October 10, 2018 19:43
@aancel
Copy link
Contributor Author

aancel commented Oct 10, 2018

Okay, rebase has been done on the latest commit. I've encountered 2 merge conflicts, I had a look at your commits, I hope to have done the things correctly with this rebase.

Regarding to the permission snackbar, I am now having the same issue as I had before.
To repoduce it on my side:

  • Remove the storage permission
  • Go in the app and click on a link
  • On my side, the snackbar pops up, then immediatly closes, so permission can't be configured

I pushed a new tiny image to the emulator by overwriting the old one, left the list (or app), returned to it and went back to the book. I saw the same image, not the new one. It's due to caching by name probably and app didn't terminate.

I don't think we need to keep the cache during entire app run. It's enough to create it when user enters the notebook (or search results). NoteListFragment might be a good place to do it as it's used by both notebook and search results.

But even then, we'll need a way to invalidate the entires. And/or store them by something other then just file name. I'd much rather have cache misses here, then stale data. Cache is much less important now with async loading. I don't consider this a blocker for this PR though. Just moving the cache from App will do for now. (I'd also suggest wrapping that whole code in a new class.)

Regarding this kind of change of the image, maybe using a hash as the storage key instead of the path could solve the issue ? Adding the overhead of hash computation, but it would then be a reliable cache

@aancel
Copy link
Contributor Author

aancel commented Oct 10, 2018

Is it #348 or similar perhaps? That would be helpful, as I wasn't able to reproduce that one.

It indeed seems to be the same thing that is happening to me. The Force UTF-8 boolean also triggers the issue for me. It would be great to also solve this issue at the same time.

@aancel
Copy link
Contributor Author

aancel commented Oct 10, 2018

Regarding the broken UI actions, I might have found something, but not so sure. I report it here so maybe it will help.

I focused on opening a notebook which works in the normal case, but not in the buggy case (meaning having toggled a button in the preference).

I used the debugger and located an area that should be the cause of not opening the notebook.
I used the debugger on this line:

LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);

In the normal case, and going trough all the functions I end up in the LocalBroadcastManager on line 255:

            ArrayList<ReceiverRecord> entries = mActions.get(intent.getAction());
            if (entries != null) {
                if (debug) Log.v(TAG, "Action list: " + entries);

Here entries is not null and thus proceeds into the code.
But in the buggy case, entries is null and it never goes in the if scope and the function returns false.

@nevenz
Copy link
Member

nevenz commented Oct 10, 2018

Regarding to the permission snackbar, I am now having the same issue as I had before.
On my side, the snackbar pops up, then immediatly closes, so permission can't be configured

Ah yes, fix is only for snackbar called from OrgFormatter, which doesn't even have an action. Cheap solution would be to wrap everything in Handler().postDelayed, so that onClick finishes first. I'll look into it again.

Regarding this kind of change of the image, maybe using a hash as the storage key instead of the path could solve the issue ? Adding the overhead of hash computation, but it would then be a reliable cache

I don't know if it would be fast enough. You'd have to open and read entire stream, calculating the hash. Not to mention you'd have to do open and read the stream again if you get a miss, unless you kept the content the first time. Though that would be done asynchronous. It's worth testing I guess.

It indeed seems to be the same thing that is happening to me. The Force UTF-8 boolean also triggers the issue for me. It would be great to also solve this issue at the same time.

It might be something to do with activities being restarted on preference change. Try disabling SharedPreferences.OnSharedPreferenceChangeListener in CommonActivity.

@aancel
Copy link
Contributor Author

aancel commented Oct 10, 2018

I've taken screenshots of the UI while debugging this gives a hint at the values when the issue occurs:
The normal case:
orgzly normal opening notebook

The case of notebook not opening:
orgzly bug opening notebook

The action associated to the intent does not seem to be found in the bad case.

In the ok case, mActions contains:

  • "com.orgzly.intent.action.UPDATING_NOTES_STARTED" -> " size = 1"
  • "com.orgzly.intent.action.DB_UPGRADE_STARTED" -> " size = 1"
  • "com.orgzly.intent.action.OPEN_QUERY" -> " size = 1"
  • "com.orgzly.intent.action.OPEN_BOOK" -> " size = 1"
  • "com.orgzly.intent.action.SHOW_SNACKBAR" -> " size = 1"
  • "com.orgzly.intent.action.BOOK_IMPORTED" -> " size = 1"
  • "com.orgzly.intent.action.OPEN_BOOKS" -> " size = 1"
  • "com.orgzly.intent.action.DB_UPGRADE_ENDED" -> " size = 1"
  • "com.orgzly.intent.action.OPEN_SETTINGS" -> " size = 1"
  • "com.orgzly.intent.action.OPEN_NOTE" -> " size = 1"
  • "com.orgzly.intent.action.OPEN_QUERIES" -> " size = 1"
  • "com.orgzly.intent.action.DB_CLEARED" -> " size = 1"
  • "com.orgzly.intent.action.SYNC_STATUS" -> " size = 1"
  • "com.orgzly.intent.action.UPDATING_NOTES_ENDED" -> " size = 1"

In the bad case, mActions contains:

  • "com.orgzly.intent.action.UPDATING_NOTES_STARTED" -> " size = 1"
  • "com.orgzly.intent.action.DB_UPGRADE_STARTED" -> " size = 1"
  • "com.orgzly.intent.action.SHOW_SNACKBAR" -> " size = 1"
  • "com.orgzly.intent.action.BOOK_IMPORTED" -> " size = 1"
  • "com.orgzly.intent.action.DB_UPGRADE_ENDED" -> " size = 1"
  • "com.orgzly.intent.action.DB_CLEARED" -> " size = 1"
  • "com.orgzly.intent.action.SYNC_STATUS" -> " size = 1"
  • "com.orgzly.intent.action.UPDATING_NOTES_ENDED" -> " size = 1"

@nevenz
Copy link
Member

nevenz commented Oct 10, 2018

The action associated to the intent does not seem to be found in the bad case.

Ah, nice find. Missing actions are registered in MainActivity#onResumeFragments.

onResumeFragments might not be the best place to do this from. Try moving that to onResume.

@aancel
Copy link
Contributor Author

aancel commented Oct 10, 2018

Latest small update refactors the code a bit, ensure that we only register the lifecycle of CommonActivity and adds a setting to update the size of fixed-width images.

I'll have a look at your comment on missing actions. I'll see if it solves the issue for me.
This huge PR might not be the right place to put this code if it solves the issue. What do you think about this ?

@nevenz
Copy link
Member

nevenz commented Oct 10, 2018

This huge PR might not be the right place to put this code if it solves the issue. What do you think about this ?

I agree, It should definitely be a new PR and we can move any further conversation to #348.

@aancel
Copy link
Contributor Author

aancel commented Oct 19, 2018

Ok for the removal of the milestone ! I'll keep on working on it as time permits (and I'll be happy with a new version too !)
Regarding to the memory issue, I think that it might come from AsyncTask that are queued up in the background. I'll have a look at the visual profiler to help debug this, thanks ! (Maybe using file.length() would help preventing too many background tasks)

I did try rebasing the PR, but yeah, it's a bit more complicated this time. I'll see how it goes, but I might end up squashing commits to make it easier on my side.

@nevenz
Copy link
Member

nevenz commented Oct 20, 2018

Ok for the removal of the milestone ! I'll keep on working on it as time permits (and I'll be happy with a new version too !)

Thanks. It's definitely not trivial.

Regarding to the memory issue, I think that it might come from AsyncTask that are queued up in the background.

Yes, there needs to be a limited number of workers, depending on the number and size of images.

We should really look into Glide, perhaps it's already solving this problem as well. It'll certainly help with some potential ListView issues (I mentioned view recycling already, I think we're now holding a reference to a View which could be long gone from display). And I see there's Target which can be implemented and probably used for setting ImageSpan.

did try rebasing the PR, but yeah, it's a bit more complicated this time. I'll see how it goes, but I might end up squashing commits to make it easier on my side.

There should be very little changes to existing code.

I'm thinking a different approach might be better. Loading images could be done outside of OrgFormatter. We can just set some new span in OrgFormatter. This (ImageLinkSpan for example) would contain information about the path and perhaps a placeholder image. Then in HeadsListViewAdapter images could be loaded and displayed instead of it.

@aancel
Copy link
Contributor Author

aancel commented Oct 20, 2018

I've given glide a try previously, but I did not seem to be working very well with ImageSpan. I can give it another try, but I'll first update the code base to the latest one.
The rebase is not going very well right now, especially in OrgFormatter.kt as there was some code refactoring (notably with FileLinkSpan), so I'm trying to update with losing anything first, before going for something new.
Right now, I can say, that due to the changes, files ending with / will be considered as files and will be linkified at first. I'll see if I can fix that later on.
Regarding ImageSpan, if we want the images to be clickable, it has to de deployed over a ClickableSpan, so over a FileLinkSpan with the changes you made.

nevenz added a commit that referenced this pull request Oct 20, 2018
@nevenz
Copy link
Member

nevenz commented Oct 20, 2018

The rebase is not going very well right now, especially in OrgFormatter.kt as there was some code refactoring (notably with FileLinkSpan), so I'm trying to update with losing anything first, before going for something new.

You don't really have to rebase, you can just continue the work and I'll merge and resolve this work later. You could just work towards the current code in master, to make resolving conflicts easier.

Or I can modify OrgFormatter for what you need if it will help you. Like exposing the view for example again. I suggest moving all the code in separate classes to get a clear idea of the interface. Even better, writing an actual interface would be perfect. I'd like to keep OrgFormatter as small and generic as possible.

Regarding ImageSpan, if we want the images to be clickable, it has to de deployed over a ClickableSpan, so over a FileLinkSpan with the changes you made.

Yes, over FileLinkSpan. And new ImageLinkSpan if we go that way. Latter could be searched for somewhere else - some place closer to the needed TextViewWithMarkup.

@aancel
Copy link
Contributor Author

aancel commented Oct 20, 2018

Yes, I see what you mean, but it's just a way to make things easier on your side.
I totally agree with keeping OrgFormatter as simple as possible, I found the previous modifications I make are not really in this direction.

Just a quick question I have regarding the setText method of the TextView. It's just to be sure about preventing issues with the async part of image loading (I didn't find the information as of now).
When the OrgFormatter.parse(...) method is called, a SpannableStringBuilder is returned and set via setText, do you know how this will be kept in memory ? As a pure CharacterSequence or do the Span objects are kept and can be updated ?
If this is raw text, there might be an issue, if the asynchronous load of an image finishes before the call to the parse function, as text might be reset (via setImageSpan).

@nevenz
Copy link
Member

nevenz commented Oct 20, 2018

When the OrgFormatter.parse(...) method is called, a SpannableStringBuilder is returned and set via setText, do you know how this will be kept in memory ? As a pure CharacterSequence or do the Span objects are kept and can be updated ?

CharacterSequence is an interface. It should be stored as SpannedString.

It can be stored as SpannableString (by passing BufferType.SPANNABLE to setText()) which is mutable, so that we could add more spans to it.

If this is raw text, there might be an issue, if the asynchronous load of an image finishes before the call to the parse function, as text might be reset (via setImageSpan).

I think we could simply do:

spannable = OrgFormatter.parse(str)
            
textView.setParsedText(spannable)
            
imageLoader.loadImages(textView)

setParsedText would keep original text and setText with BufferType.SPANNABLE

loadImages images would look for FileLinkSpan spans and add ImageSpans over them if possible/enabled/etc.

Does that sound OK? Do you need anything else apart from TextViewWithMarkup with FileLinkSpans (that have path) for the imageLoader?

@aancel
Copy link
Contributor Author

aancel commented Oct 21, 2018

Ok, thanks a lot for the input and explanations !
Was it a voluntary choice not to use BufferType.SPANNABLE with setText at first ? Or maybe is it linked to a performance issue or just the evolving use case ?

I think we could simply do:

spannable = OrgFormatter.parse(str)
            
textView.setParsedText(spannable)
            
imageLoader.loadImages(textView)

Great, it is fine for me. So if I understood well, this code would end up in the setRawText function of TextViewWithMarkup ? (unless you prefer to keep the opportunity to have images in reminders and titles)

Does that sound OK? Do you need anything else apart from TextViewWithMarkup with FileLinkSpans (that have path) for the imageLoader?

Yes, that's OK. The FileLinkSpans will do fine. I'll have a look at this !

@nevenz
Copy link
Member

nevenz commented Oct 21, 2018

Was it a voluntary choice not to use BufferType.SPANNABLE with setText at first ? Or maybe is it linked to a performance issue or just the evolving use case ?

I don't think we modify only spans anywhere right now. I think in all cases text has to be changed (even for checkbox toggling, though that could be done differently).

So if I understood well, this code would end up in the setRawText function of TextViewWithMarkup ? (unless you prefer to keep the opportunity to have images in reminders and titles)

I've made the changes for this in 813f4f9. Not exactly what I suggested, but kinda. ImageLoader.loadImages() is explicitly called where we want to - for content in the list of notes and note details.

You might want to make ImageLoader a class and create it for each TextViewWithMarkup, you might need TextViewWithMarkup in loadImage etc. It's just a quick placeholder code.

- Background load images with glide
- Add new settings:
  - Enable image display
  - Set fixed size image or enable scaling
  - Set fixed size for images
- Add function to process input paths and drawables
@aancel aancel force-pushed the update-external-file-image branch from e638efe to 1e6a95a Compare October 22, 2018 21:57
@aancel
Copy link
Contributor Author

aancel commented Oct 22, 2018

Ok, so I've rebased on your latest commit to benefit from the placeholder code you added.
The code has been reworked quite a bit and it is much simpler now. I also switched to glide, which further reduces the code size and seems to have improved the loading performance for the images (and also allows not to re-invent the wheel).
The dependency attributes on the settings have also been added.

Hopefully, I didn't break anything, but eveything seems fine on API 16, 23 and 27.

@aancel
Copy link
Contributor Author

aancel commented Oct 23, 2018

I am also keeping in mind the possibility to:

  • allow the opening of a directory via a link (This can be done by modifying the intent)
  • allow to set a base path for the file links (I'd prefer to put every file linked by orgzly, in subfolders of my orgzly repository)
    What do you think about this ? If validated, I would maybe put the modifications in an other PR ?

@nevenz
Copy link
Member

nevenz commented Oct 23, 2018

Ok, so I've rebased on your latest commit to benefit from the placeholder code you added.

Great, thanks!

Hopefully, I didn't break anything, but eveything seems fine on API 16, 23 and 27.

When I open the notebook (then go back, then open again, two times) I see:

out

If I start scrolling and go back, they all end up being the same (larger) size (which is still not large enough I think).

@nevenz
Copy link
Member

nevenz commented Oct 23, 2018

allow the opening of a directory via a link (This can be done by modifying the intent)
allow to set a base path for the file links (I'd prefer to put every file linked by orgzly, in subfolders of my orgzly repository)
What do you think about this ? If validated, I would maybe put the modifications in an other PR ?

Sure, both would be useful, new issue (if it needs more discussion) or PR would be great.

// Gather drawable information
// Scale the height by the scaledDensity to get original image size
val drawableHeight = drawable.intrinsicHeight.toFloat() * metrics.scaledDensity
val drawableWidth = drawable.intrinsicWidth.toFloat() * metrics.scaledDensity
Copy link
Member

Choose a reason for hiding this comment

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

Isn't scaledDensity for text?

Copy link
Member

Choose a reason for hiding this comment

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

Actually, why is this multiplied at all?

I just tried displaying a small icon (96x96), removing any other code below this, and it becomes a huge, low-quality image.

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, my bad.
There a few things wrong with this code indeed. The metrics.scalingDensity artifically scales the small images. The width of the view is equal to zero. The size of the Drawable are not correct regarding to the original image size (I found a way to get the original size with BitmapDrawable)
I'll rework this code

// And scaled thumbnails for faster display
.thumbnail(Glide.with(context)
.applyDefaultRequestOptions(RequestOptions().override(AppPreferences.setImageFixedWidth(context)))
.load(contentUri))
Copy link
Member

Choose a reason for hiding this comment

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

How can this improve display? It's the same image, no? I think this is meant to be for actual smaller images. It's likely we are now loading two same images twice (unless Glide is detecting it's the same uri and refusing to do so).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a very good point indeed.
Apparently and according to the documentation the thumbnail is loaded in parallel while the main image is loading, so it should indeed load the image twice.
This was part of a test that I did. I noticed that without the thumbnail code I ended up with around 80 mb of memory used (via the visual profiler) on my sample test, and with the thumbnail code, I ended up around 38 mb.
Can you confirm this behavior ?

I would guess that the main image is not loaded and only the downsampled thumbnail. I'll dig deeper to figure out what happens behind the scene. There might be a way to save memory here.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds like it could be the reason for random size images then? I think we should just drop thumbnail().


// Setup a placeholder
val drawable = ColorDrawable(Color.TRANSPARENT)
drawable.setBounds(0, 0, AppPreferences.setImageFixedWidth(context), AppPreferences.setImageFixedWidth(context))
Copy link
Member

Choose a reason for hiding this comment

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

I still get the jump for the fixed size images. Looks like this is setting the height to the width, not calculating the ratio like for the actual image display.

@aancel
Copy link
Contributor Author

aancel commented Oct 23, 2018

When I open the notebook (then go back, then open again, two times) I see:

out

If I start scrolling and go back, they all end up being the same (larger) size (which is still not large enough I think).

I also noted this behavior, it seems that the textview is not refreshed properly when changing the settings, and it does not seem to happen all the times.
I've seen that you can force a redraw with an invalidate(), but haven't had time to test it yet. Does it seem to be a good idea ?

Sure, both would be useful, new issue (if it needs more discussion) or PR would be great.

Ok, I will open a PR then !

- Switch to Bitmap/BitmapDrawable to get native image sizes
- Resize the bitmaps with Glide by pre-reading image size
@aancel
Copy link
Contributor Author

aancel commented Oct 28, 2018

(Sorry this post was written a bit in a hurry)
I've pushed a new update:

  • The thumbnail function call has been removed
  • Reading the image size before loading it with glide has been added. It allows to override the size of the bitmap stored, and reduces the choppiness with big images and memory consumption. This also allows to put a placeholder of the right size as the image loads. Regarding the performance impact, it only takes a few milliseconds (3 ms for a 5400x3600 image)
  • The images smaller than the view width should have the right size now when scaling is enabled

Let me know how it works for you
(I'm still getting jumps when going to the view, but I'm not getting the issue with the view refreshing anymore apparently)

@nevenz nevenz merged commit d870d55 into orgzly:master Oct 30, 2018
@nevenz
Copy link
Member

nevenz commented Oct 30, 2018

I've fixed few things - preferences' dependencies, images were still being scaled up, placeholder was not being set at all. There are still some issues - images are resized twice, ratio is lost on second resize in some cases, storage permission requirement needs to be made more clear, etc.

@nevenz nevenz added this to the v1.6.11 milestone Oct 31, 2018
@aancel
Copy link
Contributor Author

aancel commented Nov 3, 2018

Thank you for the merge and thanks a lot for the fixes/testing/advices you gave on this MR !

@Ypot
Copy link

Ypot commented Dec 16, 2018

I am trying to find out this weeks ago:
What is the syntax to link a file that is in the external SDcard?

@nevenz
Copy link
Member

nevenz commented Dec 19, 2018

What is the syntax to link a file that is in the external SDcard?

Files need to be inside primary storage directory (external-path for FileProvider).

It doesn't look like FileProvider supports accessing non-primary storage. There is https://issuetracker.google.com/issues/37125252

Can you open a new issue so we can track this and see if there are alternatives? Thanks.

@nevenz nevenz mentioned this pull request Dec 19, 2018
@Ypot
Copy link

Ypot commented Dec 19, 2018

Like this?
#427

@nevenz nevenz mentioned this pull request Mar 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants