Skip to content

Conversation

@mertalev
Copy link
Member

@mertalev mertalev commented Aug 30, 2025

Description

Remote thumbnails are currently fetched using a Dart client along with persistence using flutter_cache_manager.

This PR consolidates this into using more robust native clients. Not only are these clients better optimized, but they also handle caching and revalidation automatically based on HTTP cache control headers. Because of this, the PR tweaks the cache control directives to be more offline-friendly. Specifically, it allows stale items to be used for up to 30 days and be revalidated asynchronously to minimize latency.

This is in contrast to the current cache manager, which is both slow and currently has no invalidation beyond a max age of 30 days or going above cache capacity. As a mitigation for the cache manager's slow performance, cached assets are also only used as an offline fallback after the app relaunches.

The specified cache size limit is 1GiB. This is more generous than the current limit of 500 previews and 5000 thumbnails. Because of the storage savings with the new local thumbnail implementation, I think it's okay to allow more storage for remote assets, as caching is not just an optimization but also increases offline accessibility.

The PR uses a repository method to construct the HTTP clients to make it easier for these improved clients to be used elsewhere beyond thumbnails.

@mertalev mertalev changed the title feat(mobile): platform clients feat(mobile): native clients for thumbnails Aug 30, 2025
@Leptopoda
Copy link

This is indeed a very good way to improve the networking experience.

Sadly, the cronet_http package relies on google paly services and is thus not available on fdroid builds.

This package depends on Google Play Services for its Cronet implementation. To use the embedded version of Cronet without Google Play Services, see Use embedded Cronet.

The mentioned embedded version on the other hand bloats the apk size a lot (a year ago this would have been ~14MB, but it might have changed).

@shenlong-tanwen
Copy link
Member

Sadly, the cronet_http package relies on google paly services and is thus not available on fdroid builds.
The mentioned embedded version on the other hand bloats the apk size a lot (a year ago this would have been ~14MB, but it might have changed).

Our plan is to eventually move everything to using the cronet / cupertino http packages since it has better features than the dart client. Sadly, this means that the F-Droid build has to suffer from an increase in app size due to the embedded cronet stack

@shenlong-tanwen
Copy link
Member

I still face the following error for the first few assets on the timeline. The init call should probably be moved to before we make the call to runApp

I/flutter (20271): [SEVERE] [2025-09-02 11:35:03.373805] [CancellableImageProviderMixin] Error loading image
I/flutter (20271): Error: LateInitializationError: Field '_cachePath@1514447961' has not been initialized.
I/flutter (20271): [SEVERE] [2025-09-02 11:35:03.376376] [CancellableImageProviderMixin] Error loading image

@mertalev
Copy link
Member Author

mertalev commented Sep 2, 2025

I still face the following error for the first few assets on the timeline. The init call should probably be moved to before we make the call to runApp


I/flutter (20271): [SEVERE] [2025-09-02 11:35:03.373805] [CancellableImageProviderMixin] Error loading image

I/flutter (20271): Error: LateInitializationError: Field '_cachePath@1514447961' has not been initialized.

I/flutter (20271): [SEVERE] [2025-09-02 11:35:03.376376] [CancellableImageProviderMixin] Error loading image

It's done in initApp which runs before runApp. Hot reload might still be broken though 🤔

@shenlong-tanwen
Copy link
Member

It's done in initApp which runs before runApp. Hot reload might still be broken though 🤔

There are two initApp methods in the same file. This runs after runApp and the other before

@mertalev mertalev force-pushed the feat/mobile-platform-clients branch from 7fe8b12 to 11ebbe5 Compare September 19, 2025 00:16
@mertalev mertalev changed the title feat(mobile): native clients for thumbnails feat(mobile): native clients Sep 19, 2025
@alextran1502
Copy link
Member

Does this solve the problem with mLTS, custom proxy header, or self-signed cert on Android?

@alextran1502
Copy link
Member

I ran into this error while running on Android release build

W/FlutterJNI(21403): Tried to send a platform message response, but FlutterJNI was detached from native C++. Could not send. Response ID: 12
I/flutter (21403): Exception in Java code called through JNI: java.lang.IllegalStateException: Disk cache storage path already in use
I/flutter (21403): 
I/flutter (21403): java.lang.IllegalStateException: Disk cache storage path already in use
I/flutter (21403):      at org.chromium.net.impl.CronetUrlRequestContext.<init>(:com.google.android.gms.dynamite_cronetdynamite@[email protected] (260400-0):16)
I/flutter (21403):      at org.chromium.net.impl.NativeCronetEngineBuilderImpl.build(:com.google.android.gms.dynamite_cronetdynamite@[email protected] (260400-0):18)
I/flutter (21403):      at org.chromium.net.CronetEngine$Builder.build(Unknown Source:2)
I/flutter (21403):      at android.os.MessageQueue.nativePollOnce(Native Method)
I/flutter (21403):      at android.os.MessageQueue.next(MessageQueue.java:346)
I/flutter (21403):      at android.os.Looper.loopOnce(Looper.java:214)
I/flutter (21403):      at android.os.Looper.loop(Looper.java:342)
I/flutter (21403):      at android.app.ActivityThread.main(ActivityThread.java:9634)
I/flutter (21403):      at java.lang.reflect.Method.invoke(Native Method)
I/flutter (21403):      at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:619)
I/flutter (21403):      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929)
I/flutter (21403): 
I/flutter (21403): 
I/flutter (21403): #1      ProviderElementBase.watch (package:riverpod/src/framework/element.dart:742:0)
I/flutter (21403): #2      shareIntentUploadProvider.<anonymous closure> (package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart:18:0)
I/flutter (21403): #3      ProviderContainer.read (package:riverpod/src/framework/container.dart:241:0)
I/flutter (21403): #4      ConsumerStatefulElement.read (package:flutter_riverpod/src/consumer.dart:620:0)
I/flutter (21403): #5      ImmichAppState.initState (package:immich_mobile/main.dart:217:0)
I/flutter (21403): #6      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5852:0)
I/flutter (21403): #7      ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #8      Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #9      Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #10     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #11     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #12     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #13     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #14     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #15     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #16     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #17     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5884:0)
I/flutter (21403): #18     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #19     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #20     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5875:0)
I/flutter (21403): #21     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #22     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #23     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #24     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #25     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #26     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #27     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #28     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #29     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #30     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #31     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #32     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #33     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #34     _UncontrolledProviderScopeElement.mount (package:flutter_riverpod/src/framework.dart:315:0)
I/flutter (21403): #35     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #36     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #37     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #38     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5884:0)
I/flutter (21403): #39     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #40     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #41     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5875:0)
I/flutter (21403): #42     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #43     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #44     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #45     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #46     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #47     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #48     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #49     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #50     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #51     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #52     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5884:0)
I/flutter (21403): #53     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #54     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #55     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5875:0)
I/flutter (21403): #56     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #57     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #58     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #59     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #60     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #61     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #62     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #63     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #64     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #65     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #66     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5884:0)
I/flutter (21403): #67     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #68     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #69     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5875:0)
I/flutter (21403): #70     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #71     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #72     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #73     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #74     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5884:0)
I/flutter (21403): #75     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #76     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #77     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5875:0)
I/flutter (21403): #78     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #79     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #80     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #81     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #82     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #83     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #84     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #85     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #86     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #87     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #88     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5884:0)
I/flutter (21403): #89     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #90     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #91     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5875:0)
I/flutter (21403): #92     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #93     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #94     Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)
I/flutter (21403): #95     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5747:0)
I/flutter (21403): #96     Element.rebuild (package:flutter/src/widgets/framework.dart:5435:0)
I/flutter (21403): #97     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5705:0)
I/flutter (21403): #98     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5699:0)
I/flutter (21403): #99     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4548:0)
I/flutter (21403): #100    Element.updateChild (package:flutter/src/widgets/framework.dart:4004:0)

Terminate the app and reopen showed the same error

@rovo89
Copy link
Contributor

rovo89 commented Sep 20, 2025

Does this solve the problem with mLTS, custom proxy header, or self-signed cert on Android?

I just tried the latest build with mTLS and it already fails during ServerApi.pingServer() because the client certificate doesn't get sent. Which surprises me, because Chrome itself simply asks which client certificate to use... and for WebView, it's as simple as implementing a WebViewClient.onReceivedClientCertRequest().

@rovo89
Copy link
Contributor

rovo89 commented Sep 20, 2025

Sh...

void CronetURLRequest::NetworkTasks::OnCertificateRequested(
    net::URLRequest* request,
    net::SSLCertRequestInfo* cert_request_info) {
  DCHECK_CALLED_ON_VALID_THREAD(network_thread_checker_);
  // Cronet does not support client certificates.
  request->ContinueWithCertificate(nullptr, nullptr);
}

https://source.chromium.org/chromium/chromium/src/+/main:components/cronet/cronet_url_request.cc;l=241-242;drc=9f49c88dfaabb9376d2cd7bad2913d69d573a845

https://chromium.googlesource.com/chromium/src/+/abc0bae34bdc9456d7e4a1f1aa2c6aa51a4bad4d

https://issuetracker.google.com/issues/40445503

@rovo89
Copy link
Contributor

rovo89 commented Sep 20, 2025

I created https://issuetracker.google.com/issues/446368909 to ask about the chances to get support for client certificates in cronet. My guess is that it won't happen... but let's see.

I haven't tried self-signed server certificates - I assume that they would work if they have been added to the OS keystore.

For request headers, there seems to be an API. No idea about proxy headers.

@mertalev
Copy link
Member Author

Thanks for looking into it! I'm a little surprised Cronet doesn't support client certificates.

@shenlong-tanwen
Copy link
Member

FWIW, I think the dart team is also exploring the possibility of having an FFI-ed OkHttp client
https://github.com/dart-lang/http/tree/master/pkgs/ok_http
https://pub.dev/packages/ok_http

The package has only one release so far, but the repo has more activity, particularly the following PR that is more relevant to our discussion: dart-lang/http#1444

It might be worth exploring this for android instead of cronet_http in the future


There is also the fact that both the packages might suffer from crashes when used from a background flutter engine, which we do a lot: dart-lang/http#1720

The required changes for this on the framework has been merged and the maintainers said that they'll get a fix for the cronet implementation out soon. I also believe that when they do, they'd likely handle it on the OkHttp implementation of the same
dart-lang/http#1217 (comment)

@mertalev
Copy link
Member Author

mertalev commented Nov 8, 2025

There is also the fact that both the packages might suffer from crashes when used from a background flutter engine

Not sure about OkHttp, but it looks like Cronet is getting fixed.

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.

6 participants