From 3833f5f822f3f01110047a7391b1d24ea363b0b2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Apr 2025 18:51:17 -0400 Subject: [PATCH] - Refactoring services to be part of the App Lifecycle, - Migrated services to Flows that are active while their flows remain subscribed - Improved OTS Decoupling - Moved OTS verification procedures from the local cache to the data source - Migrated the forceProxy options to lambdas that return an OkHttpClient - Isolated Connectivity services, from Compose to Flow - Isolated Tor services, from Compose to Tor (only starts if the Tor option is marked as internal) - Isolated Memory trimming services, from Compose to Flow - Isolated Image Caching services, from Compose to Flow - Isolated Video Caching services - Isolated Logging services - Isolated NIP-95 Caching services - Isolated Pokey receiver services - Improved support for Tor in push notifications - Isolated OkHttpClient services as flows - Reduced the coupling of Context objects with singleton objects. - Migrated UserFeedStates, StringFeedStates and ZapFeedStates to MutableStateFlow, avoiding feed updates on Main - Forces a reconnect into relays that lost connection if any tor, connectivity or account changes - Speeds up Base64 parser --- ...ryptionTest.kt => DMFileDecryptionTest.kt} | 19 +- .../amethyst/ImageUploadTesting.kt | 33 ++- .../vitorpamplona/amethyst/OkHttpOtsTest.kt | 38 ++-- .../notifications/PushMessageReceiver.kt | 7 +- .../notifications/PushNotificationUtils.kt | 6 +- .../com/vitorpamplona/amethyst/Amethyst.kt | 198 +++++++----------- .../com/vitorpamplona/amethyst/DebugUtils.kt | 2 +- .../vitorpamplona/amethyst/ServiceManager.kt | 128 +---------- .../vitorpamplona/amethyst/model/Account.kt | 18 +- .../amethyst/model/AccountSyncedSettings.kt | 14 +- .../amethyst/model/LocalCache.kt | 14 +- .../amethyst/model/UrlCachedPreviewer.kt | 5 +- .../amethyst/service/ApplicationExt.kt | 29 +++ .../amethyst/service/CashuProcessor.kt | 20 +- .../service/Nip05NostrAddressVerifier.kt | 11 +- .../service/Nip11RelayInfoRetriever.kt | 20 +- .../service/NostrAccountDataSource.kt | 7 + .../amethyst/service/OnlineCheck.kt | 9 +- .../amethyst/service/ZapPaymentHandler.kt | 13 +- .../service/connectivity/ConnectivityFlow.kt | 94 +++++++++ .../connectivity/ConnectivityManager.kt | 55 +++++ .../connectivity/ConnectivityStatus.kt | 32 +++ .../eventCache/MemoryTrimmingService.kt | 61 ++++++ .../Base64Fetcher.kt} | 46 +--- .../BlurHashFetcher.kt} | 2 +- .../service/images/ImageCacheFactory.kt | 45 ++++ .../service/images/ImageLoaderSetup.kt | 69 ++++++ .../service/lnurl/LightningAddressResolver.kt | 16 +- .../service/logging/ChoreographerHelper.kt | 52 +++++ .../{ => service/logging}/LogMonitor.kt | 31 +-- .../amethyst/service/logging/Logging.kt | 64 ++++++ .../service/notifications/PokeyReceiver.kt | 34 ++- .../service/notifications/RegisterAccounts.kt | 7 +- .../service/okhttp/DualHttpClientManager.kt | 76 +++++++ .../service/okhttp/HttpClientManager.kt | 138 ------------ .../service/okhttp/OkHttpClientFactory.kt | 94 +++++++++ .../service/okhttp/OkHttpWebSocket.kt | 17 +- ...amExplorer.kt => OkHttpBitcoinExplorer.kt} | 52 ++--- .../amethyst/service/ots/OkHttpCalendar.kt | 6 +- .../service/ots/OkHttpCalendarAsyncSubmit.kt | 19 +- .../service/ots/OkHttpCalendarBuilder.kt | 7 +- .../service/ots/OtsBlockHeightCache.kt | 29 +++ .../service/ots/OtsResolverBuilder.kt | 52 +++++ .../playback/composable/GetVideoController.kt | 2 +- .../playback/composable/VideoViewInner.kt | 6 +- .../controls/RenderControlButtons.kt | 2 +- .../service/playback/diskCache/VideoCache.kt | 20 -- .../playback/diskCache/VideoCacheFactory.kt | 40 ++++ .../playback/playerPool/MediaSessionPool.kt | 4 +- .../playback/service/PlaybackService.kt | 15 +- .../amethyst/service/previews/UrlPreview.kt | 10 +- .../service/proxyPort/ImageProxyFlow.kt | 34 +++ .../service/proxyPort/ProxyPortFlow.kt | 93 ++++++++ .../amethyst/service/relays/RelayManager.kt | 56 +++++ .../amethyst/service/relays/RelayService.kt | 48 +++++ .../service/relays/RelayServiceStatus.kt | 33 +++ .../amethyst/service/uploads/FileHeader.kt | 7 +- .../service/uploads/ImageDownloader.kt | 18 +- .../service/uploads/UploadOrchestrator.kt | 14 +- .../uploads/blossom/BlossomUploader.kt | 14 +- .../uploads/nip95/Nip95CacheFactory.kt | 30 +++ .../service/uploads/nip96/Nip96Uploader.kt | 26 +-- .../uploads/nip96/ServerInfoRetriever.kt | 9 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 24 ++- .../amethyst/ui/actions/MediaSaverToDisk.kt | 10 +- .../amethyst/ui/actions/NewPostViewModel.kt | 3 +- .../ui/actions/NewUserMetadataViewModel.kt | 6 +- .../ui/components/ZoomableContentDialog.kt | 15 +- .../ui/components/ZoomableContentView.kt | 4 +- .../amethyst/ui/navigation/INav.kt | 4 +- .../amethyst/ui/note/types/FileStorage.kt | 2 +- .../amethyst/ui/screen/AccountScreen.kt | 3 + .../ui/screen/AccountStateViewModel.kt | 2 +- .../ui/screen/SharedPreferencesComposables.kt | 76 +------ .../amethyst/ui/screen/UserFeedState.kt | 4 +- .../amethyst/ui/screen/UserFeedView.kt | 3 +- .../amethyst/ui/screen/UserFeedViewModel.kt | 19 +- .../ui/screen/loggedIn/AccountViewModel.kt | 63 +++++- .../ui/screen/loggedIn/LoggedInPage.kt | 117 ++++++----- .../chats/feed/types/RenderEncryptedFile.kt | 4 +- .../privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../chats/privateDM/send/IMetaAttachments.kt | 5 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 4 +- .../metadata/ChannelMetadataViewModel.kt | 5 +- .../send/ChannelNewMessageViewModel.kt | 2 +- .../chats/rooms/twopane/TwoPaneNav.kt | 2 +- .../loggedIn/profile/gallery/GalleryThumb.kt | 3 +- .../loggedIn/profile/zaps/LnZapFeedState.kt | 4 +- .../loggedIn/profile/zaps/LnZapFeedView.kt | 3 +- .../profile/zaps/LnZapFeedViewModel.kt | 21 +- .../relays/common/BasicRelaySetupInfoModel.kt | 3 +- .../relays/kind3/Kind3RelayListViewModel.kt | 3 +- .../relays/nip65/Nip65RelayListViewModel.kt | 5 +- .../settings/SecurityFiltersScreen.kt | 2 +- .../loggedIn/settings/StringFeedState.kt | 4 +- .../loggedIn/settings/StringFeedView.kt | 3 +- .../loggedIn/settings/StringFeedViewModel.kt | 19 +- .../amethyst/ui/tor/TorManager.kt | 119 +++-------- .../amethyst/ui/tor/TorService.kt | 88 ++++++++ .../amethyst/ui/tor/TorServiceStatus.kt | 31 +++ .../PushNotificationReceiverService.kt | 21 +- .../notifications/PushNotificationUtils.kt | 6 +- .../ammolite/relays/NostrClient.kt | 10 + .../vitorpamplona/ammolite/relays/Relay.kt | 2 +- .../commons/base64Image/Base64Image.kt | 52 +++++ .../commons/richtext/RichTextParser.kt | 12 +- .../quartz/nip03Timestamp/ots/OtsTest.kt | 26 +-- .../relay/sockets/WebsocketBuilderFactory.kt | 5 +- .../quartz/nip03Timestamp/OtsEvent.kt | 17 +- .../quartz/nip03Timestamp/OtsResolver.kt | 14 +- .../nip03Timestamp/VerificationStateCache.kt | 11 +- 111 files changed, 1970 insertions(+), 1063 deletions(-) rename amethyst/src/androidTest/java/com/vitorpamplona/amethyst/{DMDecryptionTest.kt => DMFileDecryptionTest.kt} (79%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/ApplicationExt.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/{Base64Image.kt => images/Base64Fetcher.kt} (64%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/{BlurHashImage.kt => images/BlurHashFetcher.kt} (97%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/ChoreographerHelper.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/{ => service/logging}/LogMonitor.kt (82%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/{OkHttpBlockstreamExplorer.kt => OkHttpBitcoinExplorer.kt} (71%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsBlockHeightCache.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ImageProxyFlow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ProxyPortFlow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayManager.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayService.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayServiceStatus.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt create mode 100644 commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMDecryptionTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt similarity index 79% rename from amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMDecryptionTest.kt rename to amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt index ed80e2f77..0c53bf220 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMDecryptionTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt @@ -21,17 +21,20 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager +import com.vitorpamplona.amethyst.service.okhttp.EncryptedBlobInterceptor +import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM import junit.framework.TestCase.assertEquals +import okhttp3.OkHttpClient import okhttp3.Request import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class DMDecryptionTest { - val okHttp = HttpClientManager.getHttpClient(false) +class DMFileDecryptionTest { + // Key cache to download and decrypt encrypted files before caching them. + val keyCache = EncryptionKeyCache() val url = "https://cdn.satellite.earth/812fd4cf9d4d4b59c141ecd6a6c08c7571b5872237ad6477916cb2d119b5cacd" val cipher = @@ -44,7 +47,13 @@ class DMDecryptionTest { @Test fun runDownloadAndDecryptVideo() { - HttpClientManager.addCipherToCache(url, cipher, "video/mp4") + val client = + OkHttpClient + .Builder() + .addNetworkInterceptor(EncryptedBlobInterceptor(keyCache)) + .build() + + keyCache.add(url, cipher, "video/mp4") val request = Request @@ -54,7 +63,7 @@ class DMDecryptionTest { .get() .build() - okHttp.newCall(request).execute().use { + client.newCall(request).execute().use { assertEquals(decryptedSize, it.body.bytes().size) assertEquals(expectedMimeType, it.body.contentType().toString()) } diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index bf76e2ab6..90eca1f85 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -26,6 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.service.uploads.ImageDownloader import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader @@ -43,6 +44,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient import org.junit.Assert import org.junit.Ignore import org.junit.Test @@ -52,11 +54,21 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) class ImageUploadTesting { - val account = - Account( - AccountSettings(KeyPair()), - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - ) + companion object { + val account = + Account( + AccountSettings(KeyPair()), + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + ) + } + + val client = + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .addInterceptor(DefaultContentTypeInterceptor("Amethyst/${BuildConfig.VERSION_NAME}")) + .build() private suspend fun getBitmap(): ByteArray { val bitmap = Bitmap.createBitmap(200, 300, Bitmap.Config.ARGB_8888) @@ -94,7 +106,7 @@ class ImageUploadTesting { alt = null, sensitiveContent = null, serverBaseUrl = server.baseUrl, - forceProxy = { false }, + okHttpClient = { client }, httpAuth = account::createBlossomUploadAuth, context = InstrumentationRegistry.getInstrumentation().targetContext, ) @@ -105,7 +117,7 @@ class ImageUploadTesting { assertEquals("${server.baseUrl}/$initialHash", result.url?.removeSuffix(".png")) val imageData: ByteArray = - ImageDownloader().waitAndGetImage(result.url!!, false)?.bytes + ImageDownloader().waitAndGetImage(result.url!!, { client })?.bytes ?: run { fail("${server.name}: Should not be null") return @@ -120,7 +132,7 @@ class ImageUploadTesting { ServerInfoRetriever() .loadInfo( server.baseUrl, - false, + { client }, ) val paylod = getBitmap() @@ -134,7 +146,7 @@ class ImageUploadTesting { alt = null, sensitiveContent = null, server = serverInfo, - forceProxy = { false }, + okHttpClient = { client }, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = InstrumentationRegistry.getInstrumentation().targetContext, @@ -148,7 +160,7 @@ class ImageUploadTesting { Assert.assertTrue("${server.name}: Invalid result url", url.startsWith("http")) val imageData: ByteArray = - ImageDownloader().waitAndGetImage(url, false)?.bytes + ImageDownloader().waitAndGetImage(url, { client })?.bytes ?: run { fail("${server.name}: Should not be null") return @@ -255,6 +267,7 @@ class ImageUploadTesting { } @Test() + @Ignore("Not Working anymore/ Timeout") fun testNostrCheckBlossom() = runBlocking { testBase(ServerName("nostrcheck", "https://cdn.nostrcheck.me", ServerType.Blossom)) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt index 956ae15ad..d6503b45b 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt @@ -21,17 +21,17 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer +import com.vitorpamplona.amethyst.service.ots.OkHttpBitcoinExplorer import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder +import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver -import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps import junit.framework.TestCase.assertEquals +import okhttp3.OkHttpClient import org.junit.Assert -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import java.util.concurrent.CountDownLatch @@ -44,31 +44,37 @@ class OkHttpOtsTest { val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}" val otsEvent2Digest = "a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6" + val otsCache = OtsBlockHeightCache() - @Before - fun setup() { - OtsResolver.ots = OpenTimestamps(OkHttpBlockstreamExplorer(forceProxy = { false }), OkHttpCalendarBuilder(forceProxy = { false })) - } + val resolver = + OtsResolver( + OkHttpBitcoinExplorer( + OkHttpBitcoinExplorer.MEMPOOL_API_URL, + client = OkHttpClient.Builder().build(), + otsCache, + ), + OkHttpCalendarBuilder { OkHttpClient.Builder().build() }, + ) @Test fun verifyNostrEvent() { val ots = EventMapper.fromJson(otsEvent) as OtsEvent - println(OtsResolver.info(ots.otsByteArray())) - assertEquals(1707688818L, ots.verify()) + println(resolver.info(ots.otsByteArray())) + assertEquals(1707688818L, ots.verify(resolver)) } @Test fun verifyNostrEvent2() { val ots = EventMapper.fromJson(otsEvent2) as OtsEvent - println(OtsResolver.info(ots.otsByteArray())) - assertEquals(1706322179L, ots.verify()) + println(resolver.info(ots.otsByteArray())) + assertEquals(1706322179L, ots.verify(resolver)) } @Test fun verifyNostrPendingEvent() { val ots = EventMapper.fromJson(otsPendingEvent) as OtsEvent - println(OtsResolver.info(ots.otsByteArray())) - assertEquals(null, ots.verify()) + println(resolver.info(ots.otsByteArray())) + assertEquals(null, ots.verify(resolver)) } @Test @@ -78,7 +84,7 @@ class OkHttpOtsTest { val countDownLatch = CountDownLatch(1) - val otsFile = OtsEvent.stamp(otsEvent2Digest) + val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver) signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) { ots = it @@ -88,9 +94,9 @@ class OkHttpOtsTest { Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) println(ots!!.toJson()) - println(OtsResolver.info(ots!!.otsByteArray())) + println(resolver.info(ots.otsByteArray())) // Should not be valid because we need to wait for confirmations - assertEquals(null, ots!!.verify()) + assertEquals(null, ots.verify(resolver)) } } diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index 5190b15cb..837138d01 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -29,8 +29,6 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager -import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CancellationException @@ -91,8 +89,9 @@ class PushMessageReceiver : MessagingReceiver() { Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint") pushHandler.setEndpoint(sanitizedEndpoint) scope.launch(Dispatchers.IO) { - val okHttpClient = HttpClientManager.getHttpClient(TorManager.isSocksReady()) - PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts(), okHttpClient) + PushNotificationUtils.checkAndInit(sanitizedEndpoint, LocalPreferences.allSavedAccounts()) { + Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady()) + } notificationManager().getOrCreateZapChannel(appContext) notificationManager().getOrCreateDMChannel(appContext) } diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 8b61c4656..6deb400c6 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -33,7 +33,7 @@ object PushNotificationUtils { suspend fun checkAndInit( accounts: List, - okHttpClient: OkHttpClient, + okHttpClient: (String) -> OkHttpClient, ) = with(Dispatchers.IO) { if (!pushHandler.savedDistributorExists()) return @@ -51,7 +51,7 @@ object PushNotificationUtils { suspend fun checkAndInit( token: String, accounts: List, - okHttpClient: OkHttpClient, + okHttpClient: (String) -> OkHttpClient, ) = with(Dispatchers.IO) { // initializes if the accounts are different or if the token has changed if (hasInit?.equals(accounts) == true && lastToken == token) { @@ -63,7 +63,7 @@ object PushNotificationUtils { private suspend fun registerToken( token: String, accounts: List, - okHttpClient: OkHttpClient, + okHttpClient: (String) -> OkHttpClient, ) = retryIfException("RegisterAccounts") { RegisterAccounts(accounts, okHttpClient).go(token) lastToken = token diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index ca1385522..e630f1b57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -21,142 +21,116 @@ package com.vitorpamplona.amethyst import android.app.Application -import android.app.PendingIntent import android.content.ContentResolver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Build -import android.os.Looper -import android.os.StrictMode -import android.os.StrictMode.ThreadPolicy -import android.os.StrictMode.VmPolicy import android.util.Log -import androidx.core.net.toUri import androidx.security.crypto.EncryptedSharedPreferences -import coil3.ImageLoader import coil3.disk.DiskCache import coil3.memory.MemoryCache +import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager +import com.vitorpamplona.amethyst.service.images.ImageCacheFactory +import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.amethyst.service.logging.Logging import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket +import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache -import com.vitorpamplona.amethyst.ui.MainActivity +import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory +import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory +import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import okio.Path.Companion.toOkioPath import java.io.File -import kotlin.time.measureTimedValue class Amethyst : Application() { - val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val appAgent = "Amethyst/${BuildConfig.VERSION_NAME}" - val client: NostrClient = NostrClient(OkHttpWebSocket.BuilderFactory()) + val exceptionHandler = + CoroutineExceptionHandler { _, throwable -> + Log.e("AmethystCoroutine", "Caught exception: ${throwable.message}", throwable) + } - // Service Manager is only active when the activity is active. - val serviceManager = ServiceManager(client, applicationIOScope) + val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler) + + // Key cache to download and decrypt encrypted files before caching them. + val keyCache = EncryptionKeyCache() + + // App services that should be run as soon as there are subscribers to their flows val locationManager = LocationState(this, applicationIOScope) + val torManager = TorManager(this, applicationIOScope) + val connManager = ConnectivityManager(this, applicationIOScope) + // Service that will run at all times. val pokeyReceiver = PokeyReceiver() + val okHttpClients = + DualHttpClientManager( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + keyCache = keyCache, + scope = applicationIOScope, + ) + + val factory = + OkHttpWebSocket.BuilderFactory { _, useProxy -> + okHttpClients.getHttpClient(useProxy) + } + + val client: NostrClient = NostrClient(factory) + + val serviceManager = ServiceManager(client, applicationIOScope) + + val nip95cache: File by lazy { Nip95CacheFactory.new(this) } + val videoCache: VideoCache by lazy { VideoCacheFactory.new(this) } + val diskCache: DiskCache by lazy { ImageCacheFactory.newDisk(this) } + val memoryCache: MemoryCache by lazy { ImageCacheFactory.newMemory(this) } + + val otsVerifCache by lazy { VerificationStateCache() } + val otsBlockHeightCache by lazy { OtsBlockHeightCache() } + + override fun onCreate() { + super.onCreate() + Log.d("AmethystApp", "onCreate $this") + + instance = this + + if (isDebug()) { + Logging.setup() + } + + // initializes diskcache on an IO thread. + applicationIOScope.launch { videoCache } + + // registers to receive events + pokeyReceiver.register(this) + } + override fun onTerminate() { super.onTerminate() - unregisterReceiver(pokeyReceiver) - applicationIOScope.cancel() - } + Log.d("AmethystApp", "onTerminate $this") - fun nip95cache() = safeCacheDir.resolve("NIP95") - - val videoCache: VideoCache by lazy { - val newCache = VideoCache() - runBlocking { - newCache.initFileCache( - this@Amethyst, - safeCacheDir.resolve("exoplayer"), - ) - } - newCache - } - - val coilCache: DiskCache by lazy { - DiskCache - .Builder() - .directory(safeCacheDir.resolve("image_cache").toOkioPath()) - .maxSizePercent(0.2) - .maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB - .build() - } - - val memoryCache: MemoryCache by lazy { - MemoryCache - .Builder() - .maxSizePercent(this) - .build() + pokeyReceiver.unregister(this) + applicationIOScope.cancel("Application onTerminate $this") } fun contentResolverFn(): ContentResolver = contentResolver - override fun onCreate() { - super.onCreate() + fun isDebug() = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark" - instance = this - - if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") { - StrictMode.setThreadPolicy( - ThreadPolicy - .Builder() - .detectAll() - .penaltyLog() - .build(), - ) - StrictMode.setVmPolicy( - VmPolicy - .Builder() - .detectAll() - .penaltyLog() - .build(), - ) - Looper.getMainLooper().setMessageLogging(LogMonitor()) - ChoreographerHelper.start() + fun setImageLoader(shouldUseTor: Boolean?) = + ImageLoaderSetup.setup(this, diskCache, memoryCache, isDebug()) { + shouldUseTor?.let { okHttpClients.getHttpClient(it) } ?: okHttpClients.getHttpClient(false) } - GlobalScope.launch(Dispatchers.IO) { - val (value, elapsed) = - measureTimedValue { - // initializes the video cache in a thread - videoCache - } - Log.d("Rendering Metrics", "VideoCache initialized in $elapsed") - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver( - pokeyReceiver, - IntentFilter(PokeyReceiver.POKEY_ACTION), - RECEIVER_EXPORTED, - ) - } else { - @Suppress("UnspecifiedRegisterReceiverFlag") - registerReceiver( - pokeyReceiver, - IntentFilter(PokeyReceiver.POKEY_ACTION), - ) - } - } - - fun imageLoaderBuilder(): ImageLoader.Builder = - ImageLoader - .Builder(this) - .diskCache { coilCache } - .memoryCache { memoryCache } - fun encryptedStorage(npub: String? = null): EncryptedSharedPreferences = EncryptedStorage.preferences(instance, npub) /** @@ -164,32 +138,16 @@ class Amethyst : Application() { * * @param level the memory-related event that was raised. */ - @OptIn(DelicateCoroutinesApi::class) override fun onTrimMemory(level: Int) { super.onTrimMemory(level) - println("Trim Memory $level") - GlobalScope.launch(Dispatchers.Default) { - println("Trim Memory Inside $level") + Log.d("AmethystApp", "onTrimMemory $level") + applicationIOScope.launch(Dispatchers.Default) { serviceManager.trimMemory() } } - fun createIntent(callbackUri: String): PendingIntent = - PendingIntent.getActivity( - this, - 0, - Intent(Intent.ACTION_VIEW, callbackUri.toUri(), this, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - companion object { lateinit var instance: Amethyst private set } } - -internal val Context.safeCacheDir: File - get() { - val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" } - return cacheDir.apply { mkdirs() } - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index e075cb8e6..f0001431c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -91,7 +91,7 @@ fun debugState(context: Context) { Log.d( "STATE DUMP", - "Image Disk Cache ${(Amethyst.instance.coilCache.size) / (1024 * 1024)}/${(Amethyst.instance.coilCache.maxSize) / (1024 * 1024)} MB", + "Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB", ) Log.d( "STATE DUMP", diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt index 5b3e90977..4eebd0271 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt @@ -20,21 +20,11 @@ */ package com.vitorpamplona.amethyst -import android.os.Build import android.util.Log import androidx.compose.runtime.Stable -import coil3.SingletonImageLoader import coil3.annotation.DelicateCoilApi -import coil3.gif.AnimatedImageDecoder -import coil3.gif.GifDecoder -import coil3.network.okhttp.OkHttpNetworkFetcherFactory -import coil3.size.Precision -import coil3.svg.SvgDecoder -import coil3.util.DebugLogger import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.Base64Fetcher -import com.vitorpamplona.amethyst.service.BlurHashFetcher import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChatroomDataSource @@ -51,17 +41,10 @@ import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource import com.vitorpamplona.amethyst.service.NostrThreadDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrVideoDataSource -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager -import com.vitorpamplona.amethyst.service.ots.OkHttpBlockstreamExplorer -import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder -import com.vitorpamplona.amethyst.ui.tor.TorManager -import com.vitorpamplona.amethyst.ui.tor.TorType +import com.vitorpamplona.amethyst.service.eventCache.MemoryTrimmingService import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver -import com.vitorpamplona.quartz.nip03Timestamp.ots.OpenTimestamps import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes -import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -71,7 +54,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import java.util.concurrent.atomic.AtomicBoolean @Stable class ServiceManager( @@ -85,7 +67,7 @@ class ServiceManager( private var collectorJob: Job? = null - var isTrimmingMemoryMutex = AtomicBoolean(false) + private val trimmingService = MemoryTrimmingService() private fun start(account: Account) { this.account = account @@ -94,87 +76,17 @@ class ServiceManager( @OptIn(DelicateCoilApi::class) private fun start() { - Log.d("ServiceManager", "Pre Starting Relay Services $isStarted $account") + Log.d("ServiceManager", "-- May Start (hasStarted: $isStarted) for account $account") if (isStarted && account != null) { + Log.d("ServiceManager", "---- Restarting innactive relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}") + client.reconnect() return } - Log.d("ServiceManager", "Starting Relay Services Tor: ${account?.settings?.torSettings?.torType?.value}") + Log.d("ServiceManager", "---- Starting Relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}") val myAccount = account - // Resets Proxy Use - if (myAccount != null) { - when (myAccount.settings.torSettings.torType.value) { - TorType.INTERNAL -> { - // Tor's lib will automatically set this port. - if (TorManager.isSocksReady()) { - HttpClientManager.setDefaultProxyOnPort(TorManager.socksPort()) - } else { - HttpClientManager.setProxyNotReady() - } - } - TorType.EXTERNAL -> { - val port = myAccount.settings.torSettings.externalSocksPort.value - if (port > 0) { - HttpClientManager.setDefaultProxyOnPort(port) - } else { - HttpClientManager.setProxyNotReady() - } - } - else -> HttpClientManager.setProxyNotReady() - } - - OtsResolver.ots = - OpenTimestamps( - OkHttpBlockstreamExplorer(myAccount::shouldUseTorForMoneyOperations), - OkHttpCalendarBuilder(myAccount::shouldUseTorForMoneyOperations), - ) - } else { - OtsResolver.ots = - OpenTimestamps( - OkHttpBlockstreamExplorer { false }, - OkHttpCalendarBuilder { false }, - ) - - HttpClientManager.setProxyNotReady() - } - - // Convert this into a flow - LocalCache.antiSpam.active = account - ?.settings - ?.syncedSettings - ?.security - ?.filterSpamFromStrangers ?: true - - SingletonImageLoader.setUnsafe { - Amethyst.instance - .imageLoaderBuilder() - .components { - if (Build.VERSION.SDK_INT >= 28) { - add(AnimatedImageDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - add(SvgDecoder.Factory()) - add(Base64Fetcher.Factory) - add(BlurHashFetcher.Factory) - add(Base64Fetcher.BKeyer) - add(BlurHashFetcher.BKeyer) - add( - OkHttpNetworkFetcherFactory( - callFactory = { - myAccount?.shouldUseTorForImageDownload()?.let { HttpClientManager.getHttpClient(it) } - ?: HttpClientManager.getHttpClient(false) - }, - ), - ) - }.apply { - if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark") { - this.logger(DebugLogger()) - } - }.precision(Precision.INEXACT) - .build() - } + Amethyst.instance.setImageLoader(myAccount?.shouldUseTorForImageDownload()) if (myAccount != null) { val relaySet = myAccount.connectToRelaysWithProxy.value @@ -230,7 +142,7 @@ class ServiceManager( } private fun pause() { - Log.d("ServiceManager", "Pausing Relay Services") + Log.d("ServiceManager", "-- Pausing Relay Services") collectorJob?.cancel() collectorJob = null @@ -262,27 +174,7 @@ class ServiceManager( } suspend fun trimMemory() { - if (isTrimmingMemoryMutex.compareAndSet(false, true)) { - try { - LocalCache.cleanObservers() - - val accounts = - LocalPreferences.allSavedAccounts().mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet() - - account?.let { - LocalCache.pruneOldAndHiddenMessages(it) - NostrChatroomDataSource.clearEOSEs(it) - - LocalCache.pruneHiddenMessages(it) - LocalCache.pruneContactLists(accounts) - LocalCache.pruneRepliesAndReactions(accounts) - LocalCache.prunePastVersionsOfReplaceables() - LocalCache.pruneExpiredEvents() - } - } finally { - isTrimmingMemoryMutex.getAndSet(false) - } - } + trimmingService.run(account) } // This method keeps the pause/start in a Syncronized block to @@ -293,7 +185,7 @@ class ServiceManager( start: Boolean = true, pause: Boolean = true, ) { - Log.d("ManageRelayServices", "Force Restart $account $start $pause") + Log.d("ServiceManager", "-- Force Restart (start:$start) (pause:$pause) for $account") if (pause) { pause() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 9de50211c..f60cf2f5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS @@ -101,6 +102,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip04Dm.messages.reply import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent @@ -1215,7 +1217,7 @@ class Account( filterSpam: Boolean, ): Boolean { if (settings.updateOptOutOptions(warnReports, filterSpam)) { - if (!settings.syncedSettings.security.filterSpamFromStrangers) { + if (!settings.syncedSettings.security.filterSpamFromStrangers.value) { transientHiddenUsers.update { emptySet() } @@ -1767,8 +1769,10 @@ class Account( suspend fun updateAttestations() { Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations") + val otsResolver = otsResolver() + settings.pendingAttestations.value.forEach { pair -> - val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(pair.value), pair.key) + val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(pair.value), pair.key, otsResolver) if (otsState != null) { val hint = LocalCache.getNoteIfExists(pair.key)?.toEventHint() @@ -1802,8 +1806,9 @@ class Account( if (note.isDraft()) return val id = note.event?.id ?: note.idHex + val otsResolver = otsResolver() - settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id))) + settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver))) } fun follow(user: User) { @@ -3751,6 +3756,13 @@ class Account( fun isTrustedRelay(url: String): Boolean = connectToRelays.value.any { it.url == url } || url == settings.zapPaymentRequest?.relayUri + fun otsResolver(): OtsResolver = + OtsResolverBuilder().build( + Amethyst.instance.okHttpClients, + ::shouldUseTorForMoneyOperations, + Amethyst.instance.otsBlockHeightCache, + ) + init { Log.d("AccountRegisterObservers", "Init") settings.backupContactList?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 511b8a6bc..7aa0fef4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -49,7 +49,7 @@ class AccountSyncedSettings( AccountSecurityPreferences( MutableStateFlow(internalSettings.security.showSensitiveContent), internalSettings.security.warnAboutPostsWithReports, - internalSettings.security.filterSpamFromStrangers, + MutableStateFlow(internalSettings.security.filterSpamFromStrangers), ) fun toInternal(): AccountSyncedSettingsInternal = @@ -70,7 +70,7 @@ class AccountSyncedSettings( AccountSecurityPreferencesInternal( security.showSensitiveContent.value, security.warnAboutPostsWithReports, - security.filterSpamFromStrangers, + security.filterSpamFromStrangers.value, ), ) @@ -105,8 +105,8 @@ class AccountSyncedSettings( security.showSensitiveContent.tryEmit(syncedSettingsInternal.security.showSensitiveContent) } - if (security.filterSpamFromStrangers != syncedSettingsInternal.security.filterSpamFromStrangers) { - security.filterSpamFromStrangers = syncedSettingsInternal.security.filterSpamFromStrangers + if (security.filterSpamFromStrangers.value != syncedSettingsInternal.security.filterSpamFromStrangers) { + security.filterSpamFromStrangers.tryEmit(syncedSettingsInternal.security.filterSpamFromStrangers) } if (security.warnAboutPostsWithReports != syncedSettingsInternal.security.warnAboutPostsWithReports) { @@ -180,7 +180,7 @@ class AccountLanguagePreferences( class AccountSecurityPreferences( val showSensitiveContent: MutableStateFlow = MutableStateFlow(null), var warnAboutPostsWithReports: Boolean = true, - var filterSpamFromStrangers: Boolean = true, + var filterSpamFromStrangers: MutableStateFlow = MutableStateFlow(true), ) { fun updateShowSensitiveContent(show: Boolean?): Boolean { if (showSensitiveContent.value != show) { @@ -197,9 +197,9 @@ class AccountSecurityPreferences( warnReports: Boolean, filterSpam: Boolean, ): Boolean = - if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers) { + if (warnAboutPostsWithReports != warnReports || filterSpam != filterSpamFromStrangers.value) { warnAboutPostsWithReports = warnReports - filterSpamFromStrangers = filterSpam + filterSpamFromStrangers.tryEmit(filterSpam) true } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 4a1dc8f1d..afc02d8d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.data.DeletionIndex import com.vitorpamplona.amethyst.commons.data.LargeCache import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag +import com.vitorpamplona.amethyst.service.NostrAccountDataSource.account import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.ammolite.relays.Relay @@ -68,6 +69,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip03Timestamp.VerificationState import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent @@ -938,9 +940,6 @@ object LocalCache { // Already processed this event. if (version.event?.id == event.id) return - // makes sure the OTS has a valid certificate - if (event.cacheVerify() is VerificationState.Error) return // no valid OTS - if (version.event == null) { version.loadEvent(event, author, emptyList()) version.liveSet?.innerOts?.invalidateData() @@ -1630,7 +1629,7 @@ object LocalCache { } try { - val cachePath = Amethyst.instance.nip95cache() + val cachePath = Amethyst.instance.nip95cache cachePath.mkdirs() val file = File(cachePath, event.id) if (!file.exists()) { @@ -1978,7 +1977,10 @@ object LocalCache { .toImmutableList() } - suspend fun findEarliestOtsForNote(note: Note): Long? { + suspend fun findEarliestOtsForNote( + note: Note, + resolverBuilder: () -> OtsResolver, + ): Long? { checkNotInMainThread() var minTime: Long? = null @@ -1987,7 +1989,7 @@ object LocalCache { notes.forEach { _, item -> val noteEvent = item.event if ((noteEvent is OtsEvent && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time))) { - (noteEvent.cacheVerify() as? VerificationState.Verified)?.verifiedTime?.let { stampedTime -> + (Amethyst.instance.otsVerifCache.cacheVerify(noteEvent, resolverBuilder) as? VerificationState.Verified)?.verifiedTime?.let { stampedTime -> if (minTime == null || stampedTime < (minTime ?: Long.MAX_VALUE)) { minTime = stampedTime } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt index 2615ac2bf..933438498 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UrlCachedPreviewer.kt @@ -24,6 +24,7 @@ import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.previews.UrlPreview import com.vitorpamplona.amethyst.ui.components.UrlPreviewState +import okhttp3.OkHttpClient @Stable object UrlCachedPreviewer { @@ -32,7 +33,7 @@ object UrlCachedPreviewer { suspend fun previewInfo( url: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, onReady: suspend (UrlPreviewState) -> Unit, ) { cache[url]?.let { @@ -42,7 +43,7 @@ object UrlCachedPreviewer { UrlPreview().fetch( url, - forceProxy, + okHttpClient, onComplete = { urlInfo -> cache[url]?.let { if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ApplicationExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ApplicationExt.kt new file mode 100644 index 000000000..9fc034bd5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ApplicationExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service + +import android.app.Application +import java.io.File + +fun Application.safeCacheDir(): File { + val cacheDir = checkNotNull(cacheDir) { "cacheDir == null" } + return cacheDir.apply { mkdirs() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt index 165995c30..ba2bc569f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt @@ -28,7 +28,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -40,6 +39,7 @@ import kotlinx.serialization.cbor.ByteString import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.decodeFromByteArray import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.util.Base64 @@ -218,7 +218,7 @@ class CashuProcessor { suspend fun melt( token: CashuToken, lud16: String, - forceProxy: (url: String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context, @@ -232,12 +232,12 @@ class CashuProcessor { // Make invoice and leave room for fees milliSats = token.totalAmount * 1000, message = "Calculate Fees for Cashu", - forceProxy = forceProxy, + okHttpClient = okHttpClient, onSuccess = { baseInvoice -> feeCalculator( token.mint, baseInvoice, - forceProxy = forceProxy, + okHttpClient = okHttpClient, onSuccess = { fees -> LightningAddressResolver() .lnAddressInvoice( @@ -245,9 +245,9 @@ class CashuProcessor { // Make invoice and leave room for fees milliSats = (token.totalAmount - fees) * 1000, message = "Redeem Cashu", - forceProxy = forceProxy, + okHttpClient = okHttpClient, onSuccess = { invoice -> - meltInvoice(token, invoice, fees, forceProxy, onSuccess, onError, context) + meltInvoice(token, invoice, fees, okHttpClient, onSuccess, onError, context) }, onProgress = {}, onError = onError, @@ -268,7 +268,7 @@ class CashuProcessor { fun feeCalculator( mintAddress: String, invoice: String, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onSuccess: (Int) -> Unit, onError: (String, String) -> Unit, context: Context, @@ -277,7 +277,7 @@ class CashuProcessor { try { val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint - val client = HttpClientManager.getHttpClient(forceProxy(url)) + val client = okHttpClient(url) val factory = JsonNodeFactory.instance @@ -334,14 +334,14 @@ class CashuProcessor { token: CashuToken, invoice: String, fees: Int, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context, ) { try { val url = token.mint + "/melt" // Melt cashu tokens at Mint - val client = HttpClientManager.getHttpClient(forceProxy(url)) + val client = okHttpClient(url) val factory = JsonNodeFactory.instance diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt index 4269c3f80..98609bdd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt @@ -21,17 +21,17 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05 import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient import okhttp3.Request class Nip05NostrAddressVerifier { suspend fun fetchNip05Json( nip05: String, - forceProxy: (String) -> Boolean, + okttpClient: (String) -> OkHttpClient, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit, ) = withContext(Dispatchers.IO) { @@ -52,8 +52,7 @@ class Nip05NostrAddressVerifier { .url(url) .build() // Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint. - HttpClientManager - .getHttpClient(forceProxy(url)) + okttpClient(url) .newBuilder() .followRedirects(false) .build() @@ -78,7 +77,7 @@ class Nip05NostrAddressVerifier { suspend fun verifyNip05( nip05: String, - forceProxy: (String) -> Boolean, + okttpClient: (String) -> OkHttpClient, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit, ) { @@ -87,7 +86,7 @@ class Nip05NostrAddressVerifier { fetchNip05Json( nip05, - forceProxy, + okttpClient, onSuccess = { checkNotInMainThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index fea9d0a0b..ec9567f49 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -20,15 +20,16 @@ */ package com.vitorpamplona.amethyst.service +import android.content.ContentProviderOperation.newCall import android.util.Log import android.util.LruCache -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CancellationException import okhttp3.Call import okhttp3.Callback +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import java.io.IOException @@ -60,7 +61,7 @@ object Nip11CachedRetriever { suspend fun loadRelayInfo( dirtyUrl: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit, ) { @@ -75,24 +76,24 @@ object Nip11CachedRetriever { if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) { // just wait. } else { - retrieve(url, dirtyUrl, forceProxy, onInfo, onError) + retrieve(url, dirtyUrl, okHttpClient, onInfo, onError) } } else if (doc is RetrieveResultError) { if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) { onError(dirtyUrl, doc.error, null) } else { - retrieve(url, dirtyUrl, forceProxy, onInfo, onError) + retrieve(url, dirtyUrl, okHttpClient, onInfo, onError) } } } else { - retrieve(url, dirtyUrl, forceProxy, onInfo, onError) + retrieve(url, dirtyUrl, okHttpClient, onInfo, onError) } } private suspend fun retrieve( url: String, dirtyUrl: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit, ) { @@ -100,7 +101,7 @@ object Nip11CachedRetriever { retriever.loadRelayInfo( url = url, dirtyUrl = dirtyUrl, - forceProxy = forceProxy, + okHttpClient = okHttpClient, onInfo = { checkNotInMainThread() relayInformationDocumentCache.put(url, RetrieveResultSuccess(it)) @@ -126,7 +127,7 @@ class Nip11Retriever { suspend fun loadRelayInfo( url: String, dirtyUrl: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, onError: (String, ErrorCode, String?) -> Unit, ) { @@ -139,8 +140,7 @@ class Nip11Retriever { .url(url) .build() - HttpClientManager - .getHttpClient(forceProxy) + okHttpClient(url) .newCall(request) .enqueue( object : Callback { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index ff03bb559..46179fa34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User @@ -39,6 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -311,6 +313,11 @@ object NostrAccountDataSource : AmethystNostrDataSource("AccountData") { checkNotInMainThread() when (event) { + is OtsEvent -> { + // verifies new OTS upon arrival + Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver) + } + is PrivateOutboxRelayListEvent -> { val note = LocalCache.getAddressableNoteIfExists(event.addressTag()) val noteEvent = note?.event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt index 1fd857049..78c7b92e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -24,9 +24,9 @@ import android.util.Log import android.util.LruCache import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.quartz.utils.RandomInstance import okhttp3.EventListener +import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okio.ByteString.Companion.toByteString @@ -51,7 +51,7 @@ object OnlineChecker { fun isOnline( url: String?, - forceProxy: Boolean, + okttpClient: (String) -> OkHttpClient, ): Boolean { checkNotInMainThread() @@ -76,8 +76,7 @@ object OnlineChecker { .build() val client = - HttpClientManager - .getHttpClient(forceProxy) + okttpClient(url) .newBuilder() .eventListener(EventListener.NONE) .protocols(listOf(Protocol.HTTP_1_1)) @@ -96,7 +95,7 @@ object OnlineChecker { .get() .build() - HttpClientManager.getHttpClient(forceProxy).newCall(request).execute().use { + okttpClient(url).newCall(request).execute().use { checkNotInMainThread() it.isSuccessful } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index b089043fb..30530b4a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -44,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient import kotlin.math.round class ZapPaymentHandler( @@ -64,7 +65,7 @@ class ZapPaymentHandler( message: String, context: Context, showErrorIfNoLnAddress: Boolean, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, @@ -130,7 +131,7 @@ class ZapPaymentHandler( onProgress(0.05f) } - assembleAllInvoices(splitZapRequestPairs, amountMilliSats, message, showErrorIfNoLnAddress, forceProxy, onError, onProgress = { + assembleAllInvoices(splitZapRequestPairs, amountMilliSats, message, showErrorIfNoLnAddress, okHttpClient, onError, onProgress = { onProgress(it * 0.7f + 0.05f) // keeps within range. }, context) { payables -> if (payables.isEmpty()) { @@ -228,7 +229,7 @@ class ZapPaymentHandler( totalAmountMilliSats: Long, message: String, showErrorIfNoLnAddress: Boolean, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, context: Context, @@ -247,7 +248,7 @@ class ZapPaymentHandler( zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.inputSetup.weight, totalWeight), message = message, showErrorIfNoLnAddress = showErrorIfNoLnAddress, - forceProxy = forceProxy, + okHttpClient = okHttpClient, onError = onError, onProgressStep = { percentStepForThisPayment -> progressAllPayments += percentStepForThisPayment / requests.size @@ -319,7 +320,7 @@ class ZapPaymentHandler( zapValue: Long, message: String, showErrorIfNoLnAddress: Boolean = true, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onError: (String, String, User?) -> Unit, onProgressStep: (percent: Float) -> Unit, context: Context, @@ -341,7 +342,7 @@ class ZapPaymentHandler( milliSats = zapValue, message = message, nostrRequest = nostrZapRequest, - forceProxy = forceProxy, + okHttpClient = okHttpClient, onError = { title, msg -> onError(title, msg, toUser) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt new file mode 100644 index 000000000..3b120bced --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityFlow.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.connectivity + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.util.Log +import com.vitorpamplona.ammolite.service.checkNotInMainThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn + +class ConnectivityFlow( + val context: Context, +) { + @OptIn(FlowPreview::class) + val status = + callbackFlow { + trySend(ConnectivityStatus.Connecting) + + val connectivityManager = context.getConnectivityManager() + + Log.d("ConnectivityFlow", "Starting Connectivity Flow") + val networkCallback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + checkNotInMainThread() + Log.d("ConnectivityFlow", "onAvailable ${network.networkHandle}") + connectivityManager.getNetworkCapabilities(network)?.let { + trySend(ConnectivityStatus.Active(network.networkHandle, it.isMeteredOrMobileData())) + } + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities, + ) { + super.onCapabilitiesChanged(network, networkCapabilities) + checkNotInMainThread() + val isMobile = networkCapabilities.isMeteredOrMobileData() + Log.d("ConnectivityFlow", "onCapabilitiesChanged ${network.networkHandle} $isMobile") + trySend(ConnectivityStatus.Active(network.networkHandle, isMobile)) + } + } + + connectivityManager.registerDefaultNetworkCallback(networkCallback) + + connectivityManager.activeNetwork?.let { network -> + connectivityManager.getNetworkCapabilities(network)?.let { + trySend(ConnectivityStatus.Active(network.networkHandle, it.isMeteredOrMobileData())) + } + } + + awaitClose { + checkNotInMainThread() + Log.d("ConnectivityFlow", "Stopping Connectivity Flow") + connectivityManager.unregisterNetworkCallback(networkCallback) + trySend(ConnectivityStatus.Off) + } + }.distinctUntilChanged().debounce(200).flowOn(Dispatchers.IO) +} + +fun NetworkCapabilities.isMeteredOrMobileData(): Boolean { + val metered = !hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + val mobileData = hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + return metered || mobileData +} + +fun Context.getConnectivityManager() = getSystemService(ConnectivityManager::class.java) as ConnectivityManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt new file mode 100644 index 000000000..a9b49c7bb --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityManager.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.connectivity + +import android.app.Application +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * There should be only one instance of the Tor binding per app. + * + * Tor will connect as soon as status is listened to. + */ +class ConnectivityManager( + app: Application, + scope: CoroutineScope, +) { + val status: StateFlow = + ConnectivityFlow(app).status.stateIn( + scope, + SharingStarted.WhileSubscribed(30000), + ConnectivityStatus.Off, + ) + + val isMobileOrNull: StateFlow = + status + .map { + (status.value as? ConnectivityStatus.Active)?.isMobile + }.stateIn( + scope, + SharingStarted.WhileSubscribed(2000), + (status.value as? ConnectivityStatus.Active)?.isMobile, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt new file mode 100644 index 000000000..017d01361 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/connectivity/ConnectivityStatus.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.connectivity + +sealed class ConnectivityStatus { + data class Active( + val networkId: Long, + val isMobile: Boolean, + ) : ConnectivityStatus() + + object Off : ConnectivityStatus() + + object Connecting : ConnectivityStatus() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt new file mode 100644 index 000000000..be8d891e8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.eventCache + +import android.util.Log +import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.NostrChatroomDataSource +import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull +import java.util.concurrent.atomic.AtomicBoolean + +class MemoryTrimmingService { + var isTrimmingMemoryMutex = AtomicBoolean(false) + + private suspend fun doTrim(account: Account?) { + LocalCache.cleanObservers() + + val accounts = LocalPreferences.allSavedAccounts().mapNotNull { decodePublicKeyAsHexOrNull(it.npub) }.toSet() + + account?.let { + LocalCache.pruneHiddenMessages(it) + LocalCache.pruneOldAndHiddenMessages(it) + NostrChatroomDataSource.clearEOSEs(it) + + LocalCache.pruneContactLists(accounts) + LocalCache.pruneRepliesAndReactions(accounts) + LocalCache.prunePastVersionsOfReplaceables() + LocalCache.pruneExpiredEvents() + } + } + + suspend fun run(account: Account?) { + if (isTrimmingMemoryMutex.compareAndSet(false, true)) { + Log.d("ServiceManager", "Trimming Memory") + try { + doTrim(account) + } finally { + isTrimmingMemoryMutex.getAndSet(false) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt similarity index 64% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt index d6a5520a5..ab9c73862 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Base64Image.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt @@ -18,10 +18,8 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service +package com.vitorpamplona.amethyst.service.images -import android.content.Context -import android.graphics.BitmapFactory import androidx.compose.runtime.Stable import coil3.ImageLoader import coil3.Uri @@ -31,38 +29,22 @@ import coil3.fetch.FetchResult import coil3.fetch.Fetcher import coil3.fetch.ImageFetchResult import coil3.key.Keyer -import coil3.request.ImageRequest import coil3.request.Options -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.base64contentPattern +import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 -import java.util.Base64 @Stable class Base64Fetcher( private val options: Options, private val data: Uri, ) : Fetcher { - override suspend fun fetch(): FetchResult { - checkNotInMainThread() - - val matcher = base64contentPattern.matcher(data.toString()) - - if (matcher.find()) { - val base64String = matcher.group(2) - - val byteArray = Base64.getDecoder().decode(base64String) - val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) ?: throw Exception("Unable to load base64 $base64String") - - return ImageFetchResult( - image = bitmap.asImage(true), - isSampled = false, - dataSource = DataSource.MEMORY, - ) - } else { - throw Exception("Unable to load base64 $data") - } - } + override suspend fun fetch(): FetchResult = + ImageFetchResult( + image = Base64Image.Companion.toBitmap(data.toString()).asImage(true), + isSampled = false, + dataSource = DataSource.MEMORY, + ) object Factory : Fetcher.Factory { override fun create( @@ -89,15 +71,3 @@ class Base64Fetcher( } } } - -object Base64Requester { - fun imageRequest( - context: Context, - message: String, - ): ImageRequest = - ImageRequest - .Builder(context) - .data(message) - .fetcherFactory(Base64Fetcher.Factory) - .build() -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BlurHashImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/BlurHashFetcher.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/BlurHashImage.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/BlurHashFetcher.kt index 23716cc64..f0b6cad84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/BlurHashImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/BlurHashFetcher.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service +package com.vitorpamplona.amethyst.service.images import androidx.compose.runtime.Stable import coil3.ImageLoader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt new file mode 100644 index 000000000..c95c6301f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageCacheFactory.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.images + +import android.app.Application +import coil3.disk.DiskCache +import coil3.memory.MemoryCache +import com.vitorpamplona.amethyst.service.safeCacheDir +import okio.Path.Companion.toOkioPath + +class ImageCacheFactory { + companion object { + fun newDisk(app: Application): DiskCache = + DiskCache + .Builder() + .directory(app.safeCacheDir().resolve("image_cache").toOkioPath()) + .maxSizePercent(0.2) + .maximumMaxSizeBytes(1024 * 1024 * 1024) // 1GB + .build() + + fun newMemory(app: Application): MemoryCache = + MemoryCache + .Builder() + .maxSizePercent(app) + .build() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt new file mode 100644 index 000000000..6abadd5c7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.images + +import android.app.Application +import android.os.Build +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.disk.DiskCache +import coil3.gif.AnimatedImageDecoder +import coil3.gif.GifDecoder +import coil3.memory.MemoryCache +import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import coil3.size.Precision +import coil3.svg.SvgDecoder +import coil3.util.DebugLogger +import okhttp3.Call + +class ImageLoaderSetup { + companion object { + fun setup( + app: Application, + diskCache: DiskCache, + memoryCache: MemoryCache, + isDebug: Boolean, + callFactory: () -> Call.Factory, + ) { + SingletonImageLoader.setUnsafe( + ImageLoader + .Builder(app) + .diskCache { diskCache } + .memoryCache { memoryCache } + .precision(Precision.INEXACT) + .logger(if (isDebug) DebugLogger() else null) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(AnimatedImageDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + add(SvgDecoder.Factory()) + add(Base64Fetcher.Factory) + add(BlurHashFetcher.Factory) + add(Base64Fetcher.BKeyer) + add(BlurHashFetcher.BKeyer) + add(OkHttpNetworkFetcherFactory(callFactory)) + }.build(), + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index b212ccce1..44d5c0910 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -27,10 +27,10 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.lightning.Lud06 +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import java.math.BigDecimal @@ -55,7 +55,7 @@ class LightningAddressResolver { private fun fetchLightningAddressJson( lnaddress: String, - forceProxy: (url: String) -> Boolean, + okttpClient: (String) -> OkHttpClient, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context, @@ -76,7 +76,7 @@ class LightningAddressResolver { return } - val client = HttpClientManager.getHttpClient(forceProxy(url)) + val client = okttpClient(url) try { val request: Request = @@ -125,7 +125,7 @@ class LightningAddressResolver { milliSats: Long, message: String, nostrRequest: String? = null, - forceProxy: (url: String) -> Boolean, + okttpClient: (String) -> OkHttpClient, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, context: Context, @@ -142,7 +142,7 @@ class LightningAddressResolver { url += "&nostr=$encodedNostrRequest" } - val client = HttpClientManager.getHttpClient(forceProxy(url)) + val client = okttpClient(url) val request: Request = Request @@ -208,7 +208,7 @@ class LightningAddressResolver { milliSats: Long, message: String, nostrRequest: String? = null, - forceProxy: (url: String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onSuccess: (String) -> Unit, onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, @@ -218,7 +218,7 @@ class LightningAddressResolver { fetchLightningAddressJson( lnaddress, - forceProxy, + okHttpClient, onSuccess = { lnAddressJson -> onProgress(0.4f) @@ -259,7 +259,7 @@ class LightningAddressResolver { milliSats, message, if (allowsNostr) nostrRequest else null, - forceProxy, + okHttpClient, onSuccess = { onProgress(0.6f) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/ChoreographerHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/ChoreographerHelper.kt new file mode 100644 index 000000000..127fae2f0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/ChoreographerHelper.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.logging + +import android.util.Log +import android.view.Choreographer + +object ChoreographerHelper { + var lastFrameTimeNanos: Long = 0 + + fun start() { + Choreographer.getInstance().postFrameCallback( + object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + // Last callback time + if (lastFrameTimeNanos == 0L) { + lastFrameTimeNanos = frameTimeNanos + Choreographer.getInstance().postFrameCallback(this) + return + } + val diff = (frameTimeNanos - lastFrameTimeNanos) / 1000000 + // only report after 30ms because videos play at 30fps + if (diff > 35) { + // Follow the frame number + val droppedCount = (diff / 16.6).toInt() + Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms") + } + lastFrameTimeNanos = frameTimeNanos + Choreographer.getInstance().postFrameCallback(this) + } + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LogMonitor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/LogMonitor.kt similarity index 82% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/LogMonitor.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/LogMonitor.kt index 4c5f4250a..c23f43c2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LogMonitor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/LogMonitor.kt @@ -18,14 +18,13 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst +package com.vitorpamplona.amethyst.service.logging import android.os.Handler import android.os.HandlerThread import android.os.Looper import android.util.Log import android.util.Printer -import android.view.Choreographer import java.text.SimpleDateFormat import java.util.concurrent.atomic.AtomicBoolean @@ -168,31 +167,3 @@ class StackSampler( val TIME_FORMATTER: SimpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS") } } - -object ChoreographerHelper { - var lastFrameTimeNanos: Long = 0 - - fun start() { - Choreographer.getInstance().postFrameCallback( - object : Choreographer.FrameCallback { - override fun doFrame(frameTimeNanos: Long) { - // Last callback time - if (lastFrameTimeNanos == 0L) { - lastFrameTimeNanos = frameTimeNanos - Choreographer.getInstance().postFrameCallback(this) - return - } - val diff = (frameTimeNanos - lastFrameTimeNanos) / 1000000 - // only report after 30ms because videos play at 30fps - if (diff > 35) { - // Follow the frame number - val droppedCount = (diff / 16.6).toInt() - Log.w("block-canary", "Dropped $droppedCount frames. Skipped $diff ms") - } - lastFrameTimeNanos = frameTimeNanos - Choreographer.getInstance().postFrameCallback(this) - } - }, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt new file mode 100644 index 000000000..22a0dde20 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/Logging.kt @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.logging + +import android.os.Build +import android.os.Looper +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy +import android.os.StrictMode.VmPolicy + +class Logging { + companion object { + fun setup() { + StrictMode.setThreadPolicy( + ThreadPolicy + .Builder() + .detectAll() + .penaltyLog() + .build(), + ) + StrictMode.setVmPolicy( + VmPolicy + .Builder() + .detectLeakedSqlLiteObjects() + .detectActivityLeaks() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .detectFileUriExposure() + .detectCleartextNetwork() + .detectContentUriWithoutPermission() + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + detectCredentialProtectedWhileLocked() + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + detectIncorrectContextUse() + detectUnsafeIntentLaunch() + } + }.penaltyLog() + .build(), + ) + Looper.getMainLooper().setMessageLogging(LogMonitor()) + ChoreographerHelper.start() + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt index 168fb2e91..abdddba9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PokeyReceiver.kt @@ -20,23 +20,39 @@ */ package com.vitorpamplona.amethyst.service.notifications +import android.app.Application import android.content.BroadcastReceiver import android.content.Context +import android.content.Context.RECEIVER_EXPORTED import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.provider.LiveFolders.INTENT import android.util.Log +import androidx.core.content.ContextCompat.registerReceiver +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.quartz.nip01Core.core.Event -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch class PokeyReceiver : BroadcastReceiver() { companion object { - val POKEY_ACTION = "com.shared.NOSTR" - val TAG = "PokeyReceiver" + const val POKEY_ACTION = "com.shared.NOSTR" + const val TAG = "PokeyReceiver" + val INTENT = IntentFilter(POKEY_ACTION) } - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + fun register(app: Application) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + app.registerReceiver(this, INTENT, RECEIVER_EXPORTED) + } else { + @Suppress("UnspecifiedRegisterReceiverFlag") + app.registerReceiver(this, INTENT) + } + } + + fun unregister(app: Application) { + app.unregisterReceiver(this) + } override fun onReceive( context: Context, @@ -48,9 +64,11 @@ class PokeyReceiver : BroadcastReceiver() { if (eventStr == null) return - scope.launch(Dispatchers.IO) { + val app = context.applicationContext as Amethyst + + app.applicationIOScope.launch { try { - EventNotificationConsumer(context.applicationContext).findAccountAndConsume( + EventNotificationConsumer(app).findAccountAndConsume( Event.fromJson(eventStr), ) } catch (e: Exception) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index ad84e6e8f..59423fa84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -40,7 +40,7 @@ import kotlin.coroutines.resume class RegisterAccounts( private val accounts: List, - private val client: OkHttpClient, + private val client: (String) -> OkHttpClient, ) { val tag = if (BuildConfig.FLAVOR == "play") { @@ -139,6 +139,7 @@ class RegisterAccounts( } """ + val url = "https://push.amethyst.social/register" val mediaType = "application/json; charset=utf-8".toMediaType() val body = jsonObject.toRequestBody(mediaType) @@ -146,11 +147,11 @@ class RegisterAccounts( Request .Builder() .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") - .url("https://push.amethyst.social/register") + .url(url) .post(body) .build() - val isSucess = client.newCall(request).execute().use { it.isSuccessful } + val isSucess = client(url).newCall(request).execute().use { it.isSuccessful } Log.i(tag, "Server registration $isSucess") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt new file mode 100644 index 000000000..55963b05b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.okhttp + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import okhttp3.OkHttpClient +import java.net.InetSocketAddress +import java.net.Proxy + +class DualHttpClientManager( + userAgent: String, + proxyPortProvider: StateFlow, + isMobileDataProvider: StateFlow, + keyCache: EncryptionKeyCache, + scope: CoroutineScope, +) { + val factory = OkHttpClientFactory(keyCache) + + private val defaultHttpClient: StateFlow = + combine(proxyPortProvider, isMobileDataProvider) { proxy, mobile -> + factory.buildHttpClient(proxy, mobile, userAgent) + }.stateIn( + scope, + SharingStarted.Lazily, + factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent), + ) + + private val defaultHttpClientWithoutProxy: StateFlow = + isMobileDataProvider + .map { mobile -> + factory.buildHttpClient(mobile, userAgent) + }.stateIn( + scope, + SharingStarted.Lazily, + factory.buildHttpClient(isMobileDataProvider.value, userAgent), + ) + + fun getCurrentProxy(): Proxy? = defaultHttpClient.value.proxy + + fun getCurrentProxyPort(useProxy: Boolean): Int? = + if (useProxy) { + (getCurrentProxy()?.address() as? InetSocketAddress)?.port + } else { + null + } + + fun getHttpClient(useProxy: Boolean): OkHttpClient = + if (useProxy) { + defaultHttpClient.value + } else { + defaultHttpClientWithoutProxy.value + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt deleted file mode 100644 index 9be9f002f..000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/HttpClientManager.kt +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.service.okhttp - -import android.R.attr.port -import android.util.Log -import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher -import okhttp3.OkHttpClient -import java.net.InetSocketAddress -import java.net.Proxy -import java.time.Duration - -object HttpClientManager { - private val rootClient = - OkHttpClient - .Builder() - .followRedirects(true) - .followSslRedirects(true) - .build() - - val DEFAULT_TOR_PROXY = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) - - val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L) - val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L) - - private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI - private var defaultHttpClient: OkHttpClient? = null - private var defaultHttpClientWithoutProxy: OkHttpClient? = null - private var userAgent: String = "Amethyst/${BuildConfig.VERSION_NAME}" - - private var currentProxy: Proxy? = DEFAULT_TOR_PROXY - - private val cache = EncryptionKeyCache() - - fun setDefaultProxy(proxy: Proxy?) { - if (currentProxy != proxy) { - Log.d("HttpClient", "Changing proxy to: $proxy") - currentProxy = proxy - - // recreates singleton - defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) - } - } - - fun getCurrentProxy(): Proxy? = currentProxy - - fun setDefaultTimeout(timeout: Duration) { - Log.d("HttpClient", "Changing timeout to: $timeout") - if (defaultTimeout.seconds != timeout.seconds) { - defaultTimeout = timeout - - // recreates singleton - defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) - defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout) - } - } - - fun setDefaultUserAgent(userAgentHeader: String) { - Log.d("HttpClient", "Changing userAgent") - if (userAgent != userAgentHeader) { - userAgent = userAgentHeader - defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) - defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout) - } - } - - private fun buildHttpClient( - proxy: Proxy?, - timeout: Duration, - ): OkHttpClient { - val seconds = if (proxy != null) timeout.seconds * 3 else timeout.seconds - val duration = Duration.ofSeconds(seconds) - return rootClient - .newBuilder() - .proxy(proxy) - .readTimeout(duration) - .connectTimeout(duration) - .writeTimeout(duration) - .addInterceptor(DefaultContentTypeInterceptor(userAgent)) - .addNetworkInterceptor(LoggingInterceptor()) - .addNetworkInterceptor(EncryptedBlobInterceptor(cache)) - .build() - } - - fun getCurrentProxyPort(useProxy: Boolean): Int? = - if (useProxy) { - (currentProxy?.address() as? InetSocketAddress)?.port - } else { - null - } - - fun getHttpClient(useProxy: Boolean): OkHttpClient = - if (useProxy) { - if (defaultHttpClient == null) { - defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout) - } - defaultHttpClient!! - } else { - if (defaultHttpClientWithoutProxy == null) { - defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout) - } - defaultHttpClientWithoutProxy!! - } - - fun setProxyNotReady() { - // this blocks all connections unless Orbot is live. - setDefaultProxy(DEFAULT_TOR_PROXY) - } - - fun setDefaultProxyOnPort(port: Int) { - setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port))) - } - - fun addCipherToCache( - url: String, - cipher: NostrCipher, - expectedMimeType: String?, - ) = cache.add(url, cipher, expectedMimeType) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt new file mode 100644 index 000000000..97ddc8f24 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.okhttp + +import okhttp3.OkHttpClient +import java.net.InetSocketAddress +import java.net.Proxy +import java.time.Duration + +class OkHttpClientFactory( + val keyCache: EncryptionKeyCache, +) { + companion object { + // by picking a random proxy port, the connection will fail as it shouold. + const val DEFAULT_SOCKS_PORT: Int = 9050 + const val DEFAULT_IS_MOBILE: Boolean = false + const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 + const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 + } + + private val rootClient = + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .build() + + fun buildHttpClient( + proxy: Proxy?, + timeoutSeconds: Int, + userAgent: String, + ): OkHttpClient { + val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds + val duration = Duration.ofSeconds(seconds.toLong()) + return rootClient + .newBuilder() + .proxy(proxy) + .readTimeout(duration) + .connectTimeout(duration) + .writeTimeout(duration) + .addInterceptor(DefaultContentTypeInterceptor(userAgent)) + .addNetworkInterceptor(LoggingInterceptor()) + .addNetworkInterceptor(EncryptedBlobInterceptor(keyCache)) + .build() + } + + fun buildHttpClient( + localSocksProxyPort: Int?, + isMobile: Boolean?, + userAgent: String, + ): OkHttpClient = + buildHttpClient( + buildLocalSocksProxy(localSocksProxyPort), + buildTimeout(isMobile ?: DEFAULT_IS_MOBILE), + userAgent, + ) + + fun buildHttpClient( + isMobile: Boolean?, + userAgent: String, + ): OkHttpClient = + buildHttpClient( + null, + buildTimeout(isMobile ?: DEFAULT_IS_MOBILE), + userAgent, + ) + + fun buildTimeout(isMobile: Boolean): Int = + if (isMobile) { + DEFAULT_TIMEOUT_ON_MOBILE_SECS + } else { + DEFAULT_TIMEOUT_ON_WIFI_SECS + } + + fun buildLocalSocksProxy(port: Int?) = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port ?: DEFAULT_SOCKS_PORT)) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt index f2f48552d..94b6a4de2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt @@ -24,12 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response class OkHttpWebSocket( val url: String, val forceProxy: Boolean, + val httpClient: (url: String, forceProxy: Boolean) -> OkHttpClient, val out: WebSocketListener, ) : WebSocket { private val listener = OkHttpWebsocketListener() @@ -38,7 +40,7 @@ class OkHttpWebSocket( fun buildRequest() = Request.Builder().url(url.trim()).build() override fun connect() { - socket = HttpClientManager.getHttpClient(forceProxy).newWebSocket(buildRequest(), listener) + socket = httpClient(url, forceProxy).newWebSocket(buildRequest(), listener) } inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() { @@ -76,15 +78,22 @@ class OkHttpWebSocket( class Builder( val forceProxy: Boolean, + val httpClient: (String, Boolean) -> OkHttpClient, ) : WebsocketBuilder { + // Called when connecting. override fun build( url: String, out: WebSocketListener, - ) = OkHttpWebSocket(url, forceProxy, out) + ) = OkHttpWebSocket(url, forceProxy, httpClient, out) } - class BuilderFactory : WebsocketBuilderFactory { - override fun build(forceProxy: Boolean) = Builder(forceProxy) + class BuilderFactory( + val httpClient: (String, Boolean) -> OkHttpClient, + ) : WebsocketBuilderFactory { + override fun build( + url: String, + forceProxy: Boolean, + ) = Builder(forceProxy, httpClient) } override fun cancel() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBitcoinExplorer.kt similarity index 71% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBitcoinExplorer.kt index 86b5957a5..f3d684f06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBlockstreamExplorer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBitcoinExplorer.kt @@ -21,21 +21,19 @@ package com.vitorpamplona.amethyst.service.ots import android.util.Log -import android.util.LruCache import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException +import okhttp3.OkHttpClient import okhttp3.Request -class OkHttpBlockstreamExplorer( - val forceProxy: (String) -> Boolean, +class OkHttpBitcoinExplorer( + val baseAPI: String, + val client: OkHttpClient, + val cache: OtsBlockHeightCache, ) : BitcoinExplorer { - private val cacheHeaders = LruCache(100) - private val cacheHeights = LruCache(100) - /** * Retrieve the block information from the block hash. * @@ -44,13 +42,11 @@ class OkHttpBlockstreamExplorer( * @throws Exception desc */ override fun block(hash: String): BlockHeader { - cacheHeaders.get(hash)?.let { + cache.cacheHeaders.get(hash)?.let { return it } - val usingTor = forceProxy(BLOCKSTREAM_API_URL) - val url = "${getAPI(usingTor)}/block/$hash" - val client = HttpClientManager.getHttpClient(forceProxy(url)) + val url = "$baseAPI/block/$hash" val request = Request @@ -63,17 +59,15 @@ class OkHttpBlockstreamExplorer( client.newCall(request).execute().use { if (it.isSuccessful) { + Log.d("OkHttpBlockstreamExplorer", "$baseAPI/block/$hash") + val jsonObject = jacksonObjectMapper().readTree(it.body.string()) - val blockHeader = - BlockHeader() + val blockHeader = BlockHeader() blockHeader.merkleroot = jsonObject["merkle_root"].asText() blockHeader.setTime(jsonObject["timestamp"].asInt().toString()) blockHeader.blockHash = hash - Log.d("OkHttpBlockstreamExplorer", "$BLOCKSTREAM_API_URL/block/$hash") - - cacheHeaders.put(hash, blockHeader) - + cache.cacheHeaders.put(hash, blockHeader) return blockHeader } else { throw UrlException( @@ -92,13 +86,11 @@ class OkHttpBlockstreamExplorer( */ @Throws(Exception::class) override fun blockHash(height: Int): String { - cacheHeights[height]?.let { + cache.cacheHeights[height]?.let { return it } - val usingTor = forceProxy(BLOCKSTREAM_API_URL) - val url = "${getAPI(usingTor)}/block-height/$height" - val client = HttpClientManager.getHttpClient(usingTor) + val url = "$baseAPI/block-height/$height" val request = Request @@ -114,25 +106,19 @@ class OkHttpBlockstreamExplorer( Log.d("OkHttpBlockstreamExplorer", "$url $blockHash") - cacheHeights.put(height, blockHash) + cache.cacheHeights.put(height, blockHash) return blockHash } else { - throw UrlException( - "Couldn't open $url: " + it.message + " " + it.code, - ) + throw UrlException("Couldn't open $url: " + it.message + " " + it.code) } } } companion object { - private const val BLOCKSTREAM_API_URL = "https://blockstream.info/api" - private const val MEMPOOL_API_URL = "https://mempool.space/api/" + // doesn't accept Tor + const val BLOCKSTREAM_API_URL = "https://blockstream.info/api" - fun getAPI(usingTor: Boolean) = - if (usingTor) { - MEMPOOL_API_URL - } else { - BLOCKSTREAM_API_URL - } + // accepts Tor + const val MEMPOOL_API_URL = "https://mempool.space/api/" } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt index 475eeddfd..73a763a65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.service.ots import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp @@ -31,6 +30,7 @@ import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.ExceededSizeExcept import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException import com.vitorpamplona.quartz.utils.Hex import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody /** @@ -38,7 +38,7 @@ import okhttp3.RequestBody.Companion.toRequestBody */ class OkHttpCalendar( val url: String, - val forceProxy: Boolean, + val client: OkHttpClient, ) : ICalendar { /** * Submitting a digest to remote calendar. Returns a com.eternitywall.ots.Timestamp committing to that digest. @@ -52,7 +52,6 @@ class OkHttpCalendar( @Throws(ExceededSizeException::class, UrlException::class, DeserializationException::class) override fun submit(digest: ByteArray): Timestamp { try { - val client = HttpClientManager.getHttpClient(forceProxy) val url = "$url/digest" val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType() @@ -108,7 +107,6 @@ class OkHttpCalendar( ) override fun getTimestamp(commitment: ByteArray): Timestamp { try { - val client = HttpClientManager.getHttpClient(forceProxy) val url = url + "/timestamp/" + Hex.encode(commitment) val request = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt index eab7db495..19e0cbae7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt @@ -21,14 +21,13 @@ package com.vitorpamplona.amethyst.service.ots import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody import java.util.Optional -import java.util.concurrent.BlockingQueue /** * For making async calls to a calendar server @@ -36,19 +35,10 @@ import java.util.concurrent.BlockingQueue class OkHttpCalendarAsyncSubmit( private val url: String, private val digest: ByteArray, - private val forceProxy: Boolean, + private val client: OkHttpClient, ) : ICalendarAsyncSubmit { - private var queue: BlockingQueue>? = null - - fun setQueue(queue: BlockingQueue>?) { - this.queue = queue - } - - @Throws(Exception::class) override fun call(): Optional { - val client = HttpClientManager.getHttpClient(forceProxy) val url = "$url/digest" - val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType() val requestBody = digest.toRequestBody(mediaType) @@ -69,11 +59,8 @@ class OkHttpCalendarAsyncSubmit( it.body.bytes(), ) val timestamp = Timestamp.deserialize(ctx, digest) - val of = Optional.of(timestamp) - queue!!.add(of) - return of + return Optional.of(timestamp) } else { - queue!!.add(Optional.empty()) return Optional.empty() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt index b792d5587..800560cd2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarBuilder.kt @@ -23,14 +23,15 @@ package com.vitorpamplona.amethyst.service.ots import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarBuilder import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit +import okhttp3.OkHttpClient class OkHttpCalendarBuilder( - val forceProxy: (String) -> Boolean, + val clientFn: (url: String) -> OkHttpClient, ) : CalendarBuilder { - override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url, forceProxy(url)) + override fun newSyncCalendar(url: String): ICalendar = OkHttpCalendar(url, clientFn(url)) override fun newAsyncCalendar( url: String, digest: ByteArray, - ): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest, forceProxy(url)) + ): ICalendarAsyncSubmit = OkHttpCalendarAsyncSubmit(url, digest, clientFn(url)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsBlockHeightCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsBlockHeightCache.kt new file mode 100644 index 000000000..1d8a614a5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsBlockHeightCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.ots + +import android.util.LruCache +import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader + +class OtsBlockHeightCache { + val cacheHeaders = LruCache(100) + val cacheHeights = LruCache(100) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt new file mode 100644 index 000000000..7a872e534 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.ots + +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver + +class OtsResolverBuilder { + fun getAPI(usingTor: Boolean) = + if (usingTor) { + OkHttpBitcoinExplorer.MEMPOOL_API_URL + } else { + OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL + } + + fun build( + okHttpClients: DualHttpClientManager, + shouldUseTorForUrl: (String) -> Boolean, + cache: OtsBlockHeightCache, + ): OtsResolver { + val shouldUseTor = shouldUseTorForUrl(OkHttpBitcoinExplorer.MEMPOOL_API_URL) + + return OtsResolver( + OkHttpBitcoinExplorer( + getAPI(shouldUseTor), + okHttpClients.getHttpClient(shouldUseTor), + cache, + ), + OkHttpCalendarBuilder { + okHttpClients.getHttpClient(shouldUseTorForUrl(it)) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt index a0fa2515c..5c49807d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt @@ -61,7 +61,7 @@ fun GetVideoController( // If there is a connection, don't wait. if (!onlyOnePreparing.getAndSet(true)) { scope.launch { - Log.d("PlaybackService", "Preparing Video ${controllerId.id} $mediaItem.src.videoUri") + Log.d("PlaybackService", "Preparing Video ${controllerId.id} ${mediaItem.src.videoUri}") PlaybackServiceClient.prepareController( controllerId, mediaItem.src.videoUri, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index e560bb155..1f808f36e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -64,10 +63,7 @@ fun VideoViewInner( nostrUriCallback, mimeType, aspectRatio, - proxyPort = - HttpClientManager.getCurrentProxyPort( - accountViewModel.account.shouldUseTorForVideoDownload(videoUri), - ), + proxyPort = accountViewModel.proxyPortFor(videoUri), ) { mediaItem -> GetVideoController( mediaItem = mediaItem, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt index 4cd487f64..585b9a7fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt @@ -91,7 +91,7 @@ private fun saveMediaToGalleryInner( ) { MediaSaverToDisk.saveDownloadingIfNeeded( videoUri = videoUri, - forceProxy = accountViewModel.account.shouldUseTorForVideoDownload(), + okHttpClient = accountViewModel::okHttpClientForVideo, mimeType = mimeType, localContext = localContext, onSuccess = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt index 357f59288..1662a0453 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt @@ -32,26 +32,6 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import java.io.File -/** - * Copyright (c) 2024 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ @SuppressLint("UnsafeOptInUsageError") class VideoCache { var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 150MB diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt new file mode 100644 index 000000000..e63c53a50 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCacheFactory.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.playback.diskCache + +import android.app.Application +import com.vitorpamplona.amethyst.service.safeCacheDir +import kotlinx.coroutines.runBlocking + +class VideoCacheFactory { + companion object { + fun new(app: Application): VideoCache { + val newCache = VideoCache() + runBlocking { + newCache.initFileCache( + app, + app.safeCacheDir().resolve("exoplayer"), + ) + } + return newCache + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt index 2a9164241..7f5efa57a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt @@ -33,7 +33,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -191,7 +191,7 @@ class MediaSessionPool( // set up return call when clicking on the Notification bar mediaItems.firstOrNull()?.mediaMetadata?.extras?.getString("callbackUri")?.let { - mediaSession.setSessionActivity(Amethyst.Companion.instance.createIntent(it)) + mediaSession.setSessionActivity(MainActivity.createIntent(it)) } return Futures.immediateFuture(mediaItems) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt index 0ff0ddb4d..342cba34d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt @@ -28,7 +28,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerBuilder import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerPool @@ -60,11 +60,11 @@ class PlaybackService : MediaSessionService() { poolNoProxy?.let { return it } // creates new - return newPool(HttpClientManager.getHttpClient(false)).also { poolNoProxy = it } + return newPool(Amethyst.instance.okHttpClients.getHttpClient(false)).also { poolNoProxy = it } } else { poolWithProxy?.let { pool -> // with proxy, check if the port is the same. - val okHttp = HttpClientManager.getHttpClient(true) + val okHttp = Amethyst.instance.okHttpClients.getHttpClient(true) if (okHttp.proxy != null && okHttp.proxy == pool.exoPlayerPool.builder.okHttp.proxy) { return pool } @@ -74,12 +74,17 @@ class PlaybackService : MediaSessionService() { } // creates brand new - return newPool(HttpClientManager.getHttpClient(true)).also { poolWithProxy = it } + return newPool(Amethyst.instance.okHttpClients.getHttpClient(true)).also { poolWithProxy = it } } } + override fun onCreate() { + super.onCreate() + Log.d("PlaybackService", "PlaybackService.onCreate") + } + override fun onDestroy() { - Log.d("Lifetime Event", "PlaybackService.onDestroy") + Log.d("PlaybackService", "PlaybackService.onDestroy") poolWithProxy?.destroy() poolNoProxy?.destroy() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt index 4f9c98c47..52f4332eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt @@ -21,21 +21,21 @@ package com.vitorpamplona.amethyst.service.previews import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import okhttp3.Request class UrlPreview { suspend fun fetch( url: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, onComplete: suspend (urlInfo: UrlInfoItem) -> Unit, onFailed: suspend (t: Throwable) -> Unit, ) = try { - onComplete(getDocument(url, forceProxy)) + onComplete(getDocument(url, okHttpClient)) } catch (t: Throwable) { if (t is CancellationException) throw t onFailed(t) @@ -43,7 +43,7 @@ class UrlPreview { suspend fun getDocument( url: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, ): UrlInfoItem = withContext(Dispatchers.IO) { val request = @@ -52,7 +52,7 @@ class UrlPreview { .url(url) .get() .build() - HttpClientManager.getHttpClient(forceProxy).newCall(request).execute().use { + okHttpClient(url).newCall(request).execute().use { checkNotInMainThread() if (it.isSuccessful) { val mimeType = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ImageProxyFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ImageProxyFlow.kt new file mode 100644 index 000000000..857b5e9f2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ImageProxyFlow.kt @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.proxyPort + +import com.vitorpamplona.amethyst.Amethyst +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.onEach + +class ImageProxyFlow( + image: StateFlow, +) { + val status = + image.onEach { + Amethyst.instance.setImageLoader(it) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ProxyPortFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ProxyPortFlow.kt new file mode 100644 index 000000000..3141a0a96 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/proxyPort/ProxyPortFlow.kt @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.proxyPort + +import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus +import com.vitorpamplona.amethyst.ui.tor.TorType +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map + +class ProxyPortFlow( + torType: MutableStateFlow, + externalSocksPort: MutableStateFlow, + torServiceStatus: StateFlow, +) { + @OptIn(ExperimentalCoroutinesApi::class) + val status = + torType + .flatMapLatest { torType -> + when (torType) { + TorType.INTERNAL -> { + // subscribing to status turns Tor service on + torServiceStatus.map { + if (it is TorServiceStatus.Active) { + it.port + } else { + null + } + } + } + + TorType.EXTERNAL -> { + externalSocksPort.map { port -> + if (port > 0) { + port + } else { + null + } + } + } + + else -> MutableStateFlow(null) + } + }.distinctUntilChanged() + + companion object { + fun computePort( + torType: TorType, + externalPort: Int, + status: TorServiceStatus, + ): Int? = + when (torType) { + TorType.INTERNAL -> { + if (status is TorServiceStatus.Active && status.port > 0) { + status.port + } else { + null + } + } + + TorType.EXTERNAL -> { + if (externalPort > 0) { + externalPort + } else { + null + } + } + + else -> null + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayManager.kt new file mode 100644 index 000000000..6bcc65dca --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayManager.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relays + +import android.app.Application +import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager +import com.vitorpamplona.amethyst.ui.tor.TorManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +/** + * There should be only one instance of the Tor binding per app. + * + * Tor will connect as soon as status is listened to. + */ +class RelayManager( + app: Application, + scope: CoroutineScope, + torManager: TorManager, + connManager: ConnectivityManager, +) { + val relayService = + combine( + torManager.status, + connManager.status, + ) { torStatus, connManager -> + } + + val status: StateFlow = + RelayService(app).status.stateIn( + scope, + SharingStarted.WhileSubscribed(30000), + RelayServiceStatus.Off, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayService.kt new file mode 100644 index 000000000..9f4503874 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayService.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relays + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +class RelayService( + val context: Context, +) { + val status = + callbackFlow { + Log.d("RelayService", "Starting Relay Services") + trySend(RelayServiceStatus.Connecting) + + // ServiceManager + + awaitClose { + Log.d("RelayService", "Stopping Relay Services") + launch { + // ServiceManager.pauseAndLogOff() + } + trySend(RelayServiceStatus.Off) + } + }.distinctUntilChanged() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayServiceStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayServiceStatus.kt new file mode 100644 index 000000000..a4f6fad1a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayServiceStatus.kt @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relays + +import com.vitorpamplona.ammolite.relays.NostrClient + +sealed class RelayServiceStatus { + data class Active( + val client: NostrClient, + ) : RelayServiceStatus() + + object Off : RelayServiceStatus() + + object Connecting : RelayServiceStatus() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt index e87fbf306..b61da8bcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/FileHeader.kt @@ -26,11 +26,12 @@ import android.media.MediaDataSource import android.media.MediaMetadataRetriever import android.util.Log import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash -import com.vitorpamplona.amethyst.service.BlurhashWrapper +import com.vitorpamplona.amethyst.service.images.BlurhashWrapper import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.sha256.sha256 import kotlinx.coroutines.CancellationException +import okhttp3.OkHttpClient import java.io.IOException class FileHeader( @@ -49,10 +50,10 @@ class FileHeader( fileUrl: String, mimeType: String?, dimPrecomputed: DimensionTag?, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, ): Result = try { - val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(fileUrl, forceProxy) + val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(fileUrl, okHttpClient) if (imageData != null) { prepare(imageData.bytes, mimeType ?: imageData.contentType, dimPrecomputed) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt index 065d2178d..a515e5939 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/ImageDownloader.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.amethyst.service.uploads -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient import java.net.HttpURLConnection import java.net.URL @@ -36,7 +36,7 @@ class ImageDownloader { suspend fun waitAndGetImage( imageUrl: String, - forceProxy: Boolean, + okHttpClient: (url: String) -> OkHttpClient, ): Blob? = withContext(Dispatchers.IO) { var imageData: Blob? = null @@ -46,7 +46,7 @@ class ImageDownloader { while (imageData == null && tentatives < 15) { imageData = try { - tryGetTheImage(imageUrl, forceProxy) + tryGetTheImage(imageUrl, okHttpClient) } catch (e: Exception) { if (e is CancellationException) throw e null @@ -63,15 +63,16 @@ class ImageDownloader { private suspend fun tryGetTheImage( imageUrl: String, - forceProxy: Boolean, + okHttpClient: (url: String) -> OkHttpClient, ): Blob? = withContext(Dispatchers.IO) { // TODO: Migrate to OkHttp HttpURLConnection.setFollowRedirects(true) var url = URL(imageUrl) + var clientProxy = okHttpClient(imageUrl).proxy var huc = - if (forceProxy) { - url.openConnection(HttpClientManager.getCurrentProxy()) as HttpURLConnection + if (clientProxy != null) { + url.openConnection(clientProxy) as HttpURLConnection } else { url.openConnection() as HttpURLConnection } @@ -83,9 +84,10 @@ class ImageDownloader { // open the new connnection again url = URL(newUrl) + clientProxy = okHttpClient(newUrl).proxy huc = - if (forceProxy) { - url.openConnection(HttpClientManager.getCurrentProxy()) as HttpURLConnection + if (clientProxy != null) { + url.openConnection(clientProxy) as HttpURLConnection } else { url.openConnection() as HttpURLConnection } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index bbb09c70f..d36dbd8c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.uploads import android.content.Context import android.net.Uri +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState @@ -32,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map +import okhttp3.OkHttpClient import kotlin.coroutines.cancellation.CancellationException sealed class UploadingState { @@ -149,7 +151,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, onProgress = { percent: Float -> updateState(0.2 + (0.2 * percent), UploadingState.Uploading) }, @@ -162,7 +164,7 @@ class UploadOrchestrator { localContentType = contentType, originalContentType = contentTypeForResult, originalHash = originalHash, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, ) } catch (e: Exception) { if (e is CancellationException) throw e @@ -193,7 +195,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) @@ -201,7 +203,7 @@ class UploadOrchestrator { verifyHeader( uploadResult = result, localContentType = contentType, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, originalHash = originalHash, originalContentType = contentTypeForResult, ) @@ -216,7 +218,7 @@ class UploadOrchestrator { localContentType: String?, originalContentType: String?, originalHash: String?, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, ): UploadingFinalState { if (uploadResult.url.isNullOrBlank()) { return error(R.string.server_did_not_provide_a_url_after_uploading) @@ -224,7 +226,7 @@ class UploadOrchestrator { updateState(0.6, UploadingState.Downloading) - val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(uploadResult.url, forceProxy(uploadResult.url)) + val imageData: ImageDownloader.Blob? = ImageDownloader().waitAndGetImage(uploadResult.url, okHttpClient) if (imageData != null) { updateState(0.8, UploadingState.Hashing) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 8e76e1a1b..b92462802 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars import com.vitorpamplona.amethyst.ui.stringRes @@ -40,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okio.BufferedSink @@ -70,7 +70,7 @@ class BlossomUploader { alt: String?, sensitiveContent: String?, serverBaseUrl: String, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, httpAuth: suspend (hash: HexKey, size: Long, alt: String) -> BlossomAuthorizationEvent?, context: Context, ): MediaUploadResult { @@ -103,7 +103,7 @@ class BlossomUploader { alt, sensitiveContent, serverBaseUrl, - forceProxy, + okHttpClient, httpAuth, context, ) @@ -123,7 +123,7 @@ class BlossomUploader { alt: String?, sensitiveContent: String?, serverBaseUrl: String, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, httpAuth: suspend (hash: HexKey, size: Long, alt: String) -> BlossomAuthorizationEvent?, context: Context, ): MediaUploadResult { @@ -135,7 +135,7 @@ class BlossomUploader { val apiUrl = serverBaseUrl.removeSuffix("/") + "/upload" - val client = HttpClientManager.getHttpClient(forceProxy(apiUrl)) + val client = okHttpClient(apiUrl) val requestBuilder = Request.Builder() val requestBody: RequestBody = @@ -191,7 +191,7 @@ class BlossomUploader { hash: String, contentType: String?, serverBaseUrl: String, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, httpAuth: (hash: HexKey, alt: String) -> BlossomAuthorizationEvent?, context: Context, ): Boolean { @@ -213,7 +213,7 @@ class BlossomUploader { .delete() .build() - HttpClientManager.getHttpClient(forceProxy(apiUrl)).newCall(request).execute().use { response -> + okHttpClient(apiUrl).newCall(request).execute().use { response -> if (response.isSuccessful) { return true } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt new file mode 100644 index 000000000..3709b37b1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip95/Nip95CacheFactory.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.uploads.nip95 + +import android.app.Application +import com.vitorpamplona.amethyst.service.safeCacheDir + +class Nip95CacheFactory { + companion object { + fun new(app: Application) = app.safeCacheDir().resolve("NIP95") + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index 1ccc4b3a0..49cdab69b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag @@ -44,6 +43,7 @@ import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import kotlinx.coroutines.delay import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okio.BufferedSink @@ -62,7 +62,7 @@ class Nip96Uploader { alt: String?, sensitiveContent: String?, serverBaseUrl: String, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onProgress: (percentage: Float) -> Unit, httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?, context: Context, @@ -72,8 +72,8 @@ class Nip96Uploader { size, alt, sensitiveContent, - ServerInfoRetriever().loadInfo(serverBaseUrl, forceProxy(serverBaseUrl)), - forceProxy, + ServerInfoRetriever().loadInfo(serverBaseUrl, okHttpClient), + okHttpClient, onProgress, httpAuth, context, @@ -95,7 +95,7 @@ class Nip96Uploader { alt: String?, sensitiveContent: String?, server: ServerInfo, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onProgress: (percentage: Float) -> Unit, httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?, context: Context, @@ -117,7 +117,7 @@ class Nip96Uploader { alt, sensitiveContent, server, - forceProxy, + okHttpClient, onProgress, httpAuth, context, @@ -131,7 +131,7 @@ class Nip96Uploader { alt: String?, sensitiveContent: String?, server: ServerInfo, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onProgress: (percentage: Float) -> Unit, httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?, context: Context, @@ -141,7 +141,7 @@ class Nip96Uploader { val fileName = randomChars() val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" - val client = HttpClientManager.getHttpClient(forceProxy(server.apiUrl)) + val client = okHttpClient(server.apiUrl) val requestBuilder = Request.Builder() val requestBody: RequestBody = @@ -182,7 +182,7 @@ class Nip96Uploader { response.body.use { body -> val result = UploadResult.parse(body.string()) if (!result.processingUrl.isNullOrBlank()) { - return waitProcessing(result, server, forceProxy, onProgress) + return waitProcessing(result, server, okHttpClient, onProgress) } else if (result.status == "success") { val event = result.nip94Event if (event != null) { @@ -263,14 +263,14 @@ class Nip96Uploader { hash: String, contentType: String?, server: ServerInfo, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, httpAuth: (String, String, ByteArray?) -> HTTPAuthorizationEvent?, context: Context, ): Boolean { val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" - val client = HttpClientManager.getHttpClient(forceProxy(server.apiUrl)) + val client = okHttpClient(server.apiUrl) val requestBuilder = Request.Builder() @@ -303,7 +303,7 @@ class Nip96Uploader { private suspend fun waitProcessing( result: UploadResult, server: ServerInfo, - forceProxy: (String) -> Boolean, + okHttpClient: (String) -> OkHttpClient, onProgress: (percentage: Float) -> Unit, ): MediaUploadResult { var currentResult = result @@ -320,7 +320,7 @@ class Nip96Uploader { .url(procUrl) .build() - val client = HttpClientManager.getHttpClient(forceProxy(procUrl)) + val client = okHttpClient(procUrl) client.newCall(request).execute().use { if (it.isSuccessful) { it.body.use { currentResult = UploadResult.parse(it.string()) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt index d95119a31..2906bbe14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.service.uploads.nip96 import android.util.Log import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser import kotlinx.coroutines.CancellationException +import okhttp3.OkHttpClient import okhttp3.Request class ServerInfoRetriever { @@ -33,7 +33,7 @@ class ServerInfoRetriever { suspend fun loadInfo( baseUrl: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, ): ServerInfo { val request: Request = Request @@ -42,7 +42,10 @@ class ServerInfoRetriever { .url(parser.assembleUrl(baseUrl)) .build() - HttpClientManager.getHttpClient(forceProxy).newCall(request).execute().use { response -> + println("AABBCC $baseUrl Request ${parser.assembleUrl(baseUrl)}") + + okHttpClient(baseUrl).newCall(request).execute().use { response -> + println("AABBCC $baseUrl Response") checkNotInMainThread() response.use { val body = it.body.string() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index fe388b480..9ea808ca8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.ui +import android.app.PendingIntent +import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log @@ -28,7 +30,9 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.LaunchedEffect +import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService @@ -65,7 +69,7 @@ class MainActivity : AppCompatActivity() { enableEdgeToEdge() super.onCreate(savedInstanceState) - Log.d("Lifetime Event", "MainActivity.onCreate") + Log.d("ActivityLifecycle", "MainActivity.onCreate $this") setContent { StringResSetup() @@ -87,14 +91,14 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() - Log.d("Lifetime Event", "MainActivity.onResume") + Log.d("ActivityLifecycle", "MainActivity.onResume $this") // starts muted every time DEFAULT_MUTED_SETTING.value = true } override fun onPause() { - Log.d("Lifetime Event", "MainActivity.onPause") + Log.d("ActivityLifecycle", "MainActivity.onPause $this") GlobalScope.launch(Dispatchers.IO) { LanguageTranslatorService.clear() } @@ -111,16 +115,26 @@ class MainActivity : AppCompatActivity() { // serviceManager.trimMemory() // } - Log.d("Lifetime Event", "MainActivity.onStop") + Log.d("ActivityLifecycle", "MainActivity.onStop $this") } override fun onDestroy() { - Log.d("Lifetime Event", "MainActivity.onDestroy") + Log.d("ActivityLifecycle", "MainActivity.onDestroy $this") BackgroundMedia.removeBackgroundControllerAndReleaseIt() super.onDestroy() } + + companion object { + fun createIntent(callbackUri: String): PendingIntent = + PendingIntent.getActivity( + Amethyst.instance, + 0, + Intent(Intent.ACTION_VIEW, callbackUri.toUri(), Amethyst.instance, MainActivity::class.java), + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + } } fun uriToRoute(uri: String?): Route? = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt index d79dd754c..655f03876 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt @@ -32,10 +32,10 @@ import androidx.annotation.RequiresApi import androidx.core.net.toFile import androidx.core.net.toUri import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import kotlinx.coroutines.CancellationException import okhttp3.Call import okhttp3.Callback +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okio.BufferedSource @@ -49,7 +49,7 @@ import java.util.UUID object MediaSaverToDisk { fun saveDownloadingIfNeeded( videoUri: String?, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, mimeType: String?, localContext: Context, onSuccess: () -> Any?, @@ -69,7 +69,7 @@ object MediaSaverToDisk { downloadAndSave( url = videoUri, mimeType = mimeType, - forceProxy = forceProxy, + okHttpClient = okHttpClient, context = localContext, onSuccess = onSuccess, onError = onError, @@ -85,12 +85,12 @@ object MediaSaverToDisk { fun downloadAndSave( url: String, mimeType: String?, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, context: Context, onSuccess: () -> Any?, onError: (Throwable) -> Any?, ) { - val client = HttpClientManager.getHttpClient(forceProxy) + val client = okHttpClient(url) val request = Request diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index b34c753df..76fd0075a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.actions +import android.R.attr.category import android.content.Context import android.util.Log import androidx.compose.runtime.Stable @@ -1191,7 +1192,7 @@ open class NewPostViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( item.url.url, - accountViewModel?.account?.shouldUseTorForImageDownload() ?: false, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index fc2cd0a0d..f0e7b2ecf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -26,6 +26,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import coil3.util.CoilUtils.result +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.CompressorQuality @@ -185,7 +187,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -198,7 +200,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index ea1c383f7..717b031dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -308,17 +308,16 @@ private fun saveMediaToGallery( val failure = if (isImage) R.string.failed_to_save_the_image else R.string.failed_to_save_the_video if (content is MediaUrlContent) { - val useTor = - if (isImage) { - accountViewModel.account.shouldUseTorForImageDownload() - } else { - accountViewModel.account.shouldUseTorForVideoDownload() - } - MediaSaverToDisk.downloadAndSave( content.url, mimeType = content.mimeType, - forceProxy = useTor, + okHttpClient = { + if (isImage) { + accountViewModel.okHttpClientForImage(it) + } else { + accountViewModel.okHttpClientForVideo(it) + } + }, localContext, onSuccess = { accountViewModel.toastManager.toast(success, success) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 9823d76e3..34fcb00d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -79,7 +79,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.model.MediaAspectRatioCache -import com.vitorpamplona.amethyst.service.BlurhashWrapper +import com.vitorpamplona.amethyst.service.images.BlurhashWrapper import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.InformationDialog @@ -739,7 +739,7 @@ fun ShareImageAction( private suspend fun verifyHash(content: MediaUrlContent): Boolean? { if (content.hash == null) return null - Amethyst.instance.coilCache.openSnapshot(content.url)?.use { snapshot -> + Amethyst.instance.diskCache.openSnapshot(content.url)?.use { snapshot -> val hash = sha256(snapshot.data.toFile().readBytes()).toHexKey() Log.d("Image Hash Verification", "$hash == ${content.hash}") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/INav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/INav.kt index c07c06f1e..26c37269b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/INav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/INav.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.navigation +import android.annotation.SuppressLint import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.runtime.Composable @@ -118,13 +119,14 @@ class Nav( } } + @SuppressLint("RestrictedApi") override fun popUpTo( route: Route, upToClass: KClass, ) { scope.launch { controller.navigate(route) { - popUpTo(upToClass) { inclusive = true } + popUpTo(upToClass) { inclusive = true } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index dca60e57e..02c39c05b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -77,7 +77,7 @@ private fun ObserverAndRenderNIP95( // Creates a new object when the event arrives to force an update of the image. val note = noteState?.note val uri = header.toNostrUri() - val localDir = note?.idHex?.let { File(Amethyst.instance.nip95cache(), it) } + val localDir = note?.idHex?.let { File(Amethyst.instance.nip95cache, it) } val blurHash = eventHeader.blurhash() val dimensions = eventHeader.dimensions() val description = eventHeader.alt() ?: eventHeader.content diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 8f66fb101..0379b09c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen +import android.util.Log import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement @@ -45,6 +46,8 @@ fun AccountScreen( ) { val accountState by accountStateViewModel.accountContent.collectAsStateWithLifecycle() + Log.d("ManageRelayServices", "AccountScreen $accountState $accountStateViewModel") + Crossfade( targetState = accountState, animationSpec = tween(durationMillis = 100), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 0d12a4985..4b2f623ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -234,7 +234,7 @@ class AccountStateViewModel : ViewModel() { } else if (EMAIL_PATTERN.matcher(key).matches()) { Nip05NostrAddressVerifier().verifyNip05( key, - forceProxy = { false }, + okttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) }, onSuccess = { publicKey -> loginSync(Hex.decode(publicKey).toNpub(), torSettings, transientAccount, loginWithExternalSigner, packageName, onError) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesComposables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesComposables.kt index 45c48693a..769977210 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesComposables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesComposables.kt @@ -20,26 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities -import android.util.Log import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.adaptive.calculateDisplayFeatures +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus import com.vitorpamplona.amethyst.ui.components.getActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @Composable fun prepareSharedViewModel(): SharedPreferencesViewModel { @@ -70,61 +61,12 @@ fun MonitorDisplaySize(sharedPreferencesViewModel: SharedPreferencesViewModel) { @Composable fun ManageConnectivity(sharedPreferencesViewModel: SharedPreferencesViewModel) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - var job = remember { null } + val status = + Amethyst.instance.connManager.status + .collectAsStateWithLifecycle() - val networkCallback = - remember { - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - super.onAvailable(network) - sharedPreferencesViewModel.updateNetworkState(network.networkHandle) - } - - // Network capabilities have changed for the network - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities, - ) { - super.onCapabilitiesChanged(network, networkCapabilities) - sharedPreferencesViewModel.updateNetworkState(network.networkHandle) - sharedPreferencesViewModel.updateConnectivityStatusState(networkCapabilities) - } - } - } - - LifecycleResumeEffect(sharedPreferencesViewModel) { - job?.cancel() - job = - scope.launch(Dispatchers.IO) { - Log.d("ManageConnectivity", "Register network listener from Resume") - val connectivityManager = context.getConnectivityManager() - connectivityManager.registerDefaultNetworkCallback(networkCallback) - connectivityManager.activeNetwork?.let { network -> - sharedPreferencesViewModel.updateNetworkState(network.networkHandle) - connectivityManager.getNetworkCapabilities(network)?.let { - sharedPreferencesViewModel.updateConnectivityStatusState(it) - } - } - } - - onPauseOrDispose { - job?.cancel() - job = - scope.launch(Dispatchers.IO) { - delay(30000) // 30 seconds - Log.d("ManageConnectivity", "Unregister network listener from Pause") - context.getConnectivityManager().unregisterNetworkCallback(networkCallback) - } - } + (status.value as? ConnectivityStatus.Active)?.let { + sharedPreferencesViewModel.updateNetworkState(it.networkId) + sharedPreferencesViewModel.updateConnectivityStatusState(it.isMobile) } } - -fun SharedPreferencesViewModel.updateConnectivityStatusState(networkCapabilities: NetworkCapabilities) { - val metered = !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) - val mobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) - updateConnectivityStatusState(metered || mobileData) -} - -fun Context.getConnectivityManager() = getSystemService(ConnectivityManager::class.java) as ConnectivityManager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt index ceea2886e..e54ddce58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt @@ -20,15 +20,15 @@ */ package com.vitorpamplona.amethyst.ui.screen -import androidx.compose.runtime.MutableState import com.vitorpamplona.amethyst.model.User import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.MutableStateFlow sealed class UserFeedState { object Loading : UserFeedState() class Loaded( - val feed: MutableState>, + val feed: MutableStateFlow>, ) : UserFeedState() object Empty : UserFeedState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 27f7ceb1f..23240af96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -81,13 +81,14 @@ private fun FeedLoaded( accountViewModel: AccountViewModel, nav: INav, ) { + val items by state.feed.collectAsStateWithLifecycle() val listState = rememberLazyListState() LazyColumn( contentPadding = FeedPadding, state = listState, ) { - itemsIndexed(state.feed.value, key = { _, item -> item.pubkeyHex }) { _, item -> + itemsIndexed(items, key = { _, item -> item.pubkeyHex }) { _, item -> UserCompose(item, accountViewModel = accountViewModel, nav = nav) HorizontalDivider( thickness = DividerThickness, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 3027ee01a..ad50a9b12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -43,7 +43,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class NostrHiddenAccountsFeedViewModel( @@ -103,16 +102,14 @@ open class UserFeedViewModel( } private fun updateFeed(notes: ImmutableList) { - viewModelScope.launch(Dispatchers.Main) { - val currentState = _feedContent.value - if (notes.isEmpty()) { - _feedContent.update { UserFeedState.Empty } - } else if (currentState is UserFeedState.Loaded) { - // updates the current list - currentState.feed.value = notes - } else { - _feedContent.update { UserFeedState.Loaded(mutableStateOf(notes)) } - } + val currentState = _feedContent.value + if (notes.isEmpty()) { + _feedContent.tryEmit(UserFeedState.Empty) + } else if (currentState is UserFeedState.Loaded) { + // updates the current list + currentState.feed.tryEmit(notes) + } else { + _feedContent.tryEmit(UserFeedState.Loaded(MutableStateFlow(notes))) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index eb26121ea..6da9f36ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.service.proxyPort.ProxyPortFlow import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager @@ -136,6 +137,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient @Stable class AccountViewModel( @@ -145,6 +147,21 @@ class AccountViewModel( Dao { val account = Account(accountSettings, accountSettings.createSigner(), viewModelScope) + val proxyPortLogic = + ProxyPortFlow( + account.settings.torSettings.torType, + account.settings.torSettings.externalSocksPort, + Amethyst.instance.torManager.status, + ).status.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(30000), + ProxyPortFlow.computePort( + account.settings.torSettings.torType.value, + account.settings.torSettings.externalSocksPort.value, + Amethyst.instance.torManager.status.value, + ), + ) + var firstRoute: Route? = null // TODO: contact lists are not notes yet @@ -675,7 +692,7 @@ class AccountViewModel( message = message, context = context, showErrorIfNoLnAddress = showErrorIfNoLnAddress, - forceProxy = account::shouldUseTorForMoneyOperations, + okHttpClient = ::okHttpClientForMoney, onError = onError, onProgress = { onProgress(it) @@ -903,7 +920,7 @@ class AccountViewModel( ) { viewModelScope.launch(Dispatchers.IO) { if (account.updateOptOutOptions(warnReports, filterSpam)) { - LocalCache.antiSpam.active = filterSpamFromStrangers() + LocalCache.antiSpam.active = filterSpamFromStrangers().value } } } @@ -962,7 +979,7 @@ class AccountViewModel( onResult: suspend (UrlPreviewState) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { - UrlCachedPreviewer.previewInfo(url, account.shouldUseTorForPreviewUrl(url), onResult) + UrlCachedPreviewer.previewInfo(url, ::okHttpClientForPreview, onResult) } } @@ -983,7 +1000,9 @@ class AccountViewModel( Nip05NostrAddressVerifier() .verifyNip05( nip05, - forceProxy = account::shouldUseTorForNIP05, + okttpClient = { + Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP05(it)) + }, onSuccess = { // Marks user as verified if (it == pubkeyHex) { @@ -1020,7 +1039,7 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { Nip11CachedRetriever.loadRelayInfo( dirtyUrl, - account.shouldUseTorForDirty(dirtyUrl), + okHttpClient = ::okHttpClientForDirty, onInfo, onError, ) @@ -1110,7 +1129,7 @@ class AccountViewModel( ) { onResult( withContext(Dispatchers.Default) { - LocalCache.findEarliestOtsForNote(note) + LocalCache.findEarliestOtsForNote(note, account::otsResolver) }, ) } @@ -1197,7 +1216,9 @@ class AccountViewModel( onDone: (Boolean) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { - onDone(OnlineChecker.isOnline(videoUrl, account.shouldUseTorForVideoDownload(videoUrl))) + onDone( + OnlineChecker.isOnline(videoUrl, ::okHttpClientForVideo), + ) } } @@ -1278,6 +1299,12 @@ class AccountViewModel( Amethyst.instance.serviceManager.pauseForGood() } + fun pauseAndLogOff() = + viewModelScope.launch(Dispatchers.IO) { + Amethyst.instance.serviceManager.cleanObservers() + Amethyst.instance.serviceManager.pauseAndLogOff() + } + fun changeProxyPort(port: Int) = viewModelScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.forceRestart() @@ -1399,7 +1426,7 @@ class AccountViewModel( .melt( token, lud16, - forceProxy = account::shouldUseTorForMoneyOperations, + okHttpClient = ::okHttpClientForMoney, onSuccess = { title, message -> onDone(title, message) }, onError = { title, message -> onDone(title, message) }, context, @@ -1491,6 +1518,22 @@ class AccountViewModel( } } + fun proxyPortFor(url: String): Int? = Amethyst.instance.okHttpClients.getCurrentProxyPort(account.shouldUseTorForVideoDownload(url)) + + fun okHttpClientForNip96(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url)) + + fun okHttpClientForImage(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload()) + + fun okHttpClientForVideo(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url)) + + fun okHttpClientForMoney(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForMoneyOperations(url)) + + fun okHttpClientForPreview(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForPreviewUrl(url)) + + fun okHttpClientForDirty(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(url)) + + fun okHttpClientForTrustedRelays(url: String): OkHttpClient = Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForTrustedRelays()) + suspend fun deleteDraft(draftTag: String) { account.deleteDraft(draftTag) } @@ -1607,7 +1650,7 @@ class AccountViewModel( milliSats, message, null, - forceProxy = account::shouldUseTorForMoneyOperations, + okHttpClient = ::okHttpClientForMoney, onSuccess = onSuccess, onError = onError, onProgress = onProgress, @@ -1622,7 +1665,7 @@ class AccountViewModel( milliSats, message, zapRequest.toJson(), - forceProxy = account::shouldUseTorForMoneyOperations, + okHttpClient = ::okHttpClientForMoney, onSuccess = onSuccess, onError = onError, onProgress = onProgress, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 62e8a9bda..35b99b6eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -30,7 +30,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LifecycleResumeEffect @@ -41,21 +40,18 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel -import com.vitorpamplona.amethyst.ui.tor.TorManager -import com.vitorpamplona.amethyst.ui.tor.TorStatus +import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -77,8 +73,12 @@ fun LoggedInPage( ), ) + Log.d("ManageRelayServices", "LoggedInPage $accountViewModel") + accountViewModel.firstRoute = route + ManageSpamFilters(accountViewModel) + ManageRelayServices(accountViewModel, sharedPreferencesViewModel) ManageTorInstance(accountViewModel) @@ -94,39 +94,65 @@ fun LoggedInPage( ) } +@Composable +fun ManageSpamFilters(accountViewModel: AccountViewModel) { + val isSpamActive by accountViewModel.account.settings.syncedSettings.security.filterSpamFromStrangers + .collectAsStateWithLifecycle(true) + + LocalCache.antiSpam.active = isSpamActive +} + @Composable fun ManageRelayServices( accountViewModel: AccountViewModel, sharedPreferencesViewModel: SharedPreferencesViewModel, ) { - var job = remember { null } - LaunchedEffect( sharedPreferencesViewModel.sharedPrefs.currentNetworkId, sharedPreferencesViewModel.sharedPrefs.isOnMobileOrMeteredConnection, ) { - Log.d("ManageRelayServices", "Change Network Id/State ${sharedPreferencesViewModel.sharedPrefs.currentNetworkId}, forcing restart") - if (sharedPreferencesViewModel.sharedPrefs.isOnMobileOrMeteredConnection) { - HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_MOBILE) - } else { - HttpClientManager.setDefaultTimeout(HttpClientManager.DEFAULT_TIMEOUT_ON_WIFI) - } + Log.d("ManageRelayServices", "Loading/Change Network Id/State ${sharedPreferencesViewModel.sharedPrefs.currentNetworkId}, forcing start/restart of the relay services") accountViewModel.forceRestartServices() } - LifecycleResumeEffect(sharedPreferencesViewModel, accountViewModel) { - job?.cancel() - Log.d("ManageRelayServices", "Starting Relay Services") - job = accountViewModel.justStart() + val lifeCycleOwner = LocalLifecycleOwner.current - onPauseOrDispose { - job?.cancel() - job = - GlobalScope.launch(Dispatchers.IO) { - delay(30000) // 30 seconds - Log.d("ManageRelayServices", "Pausing Relay Services") - accountViewModel.justPause() + val scope = rememberCoroutineScope() + var job = remember { null } + + Log.d("ManageRelayServices", "Job $job for $accountViewModel") + + DisposableEffect(key1 = accountViewModel) { + job?.cancel() + val observer = + LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + job?.cancel() + Log.d("ManageRelayServices", "Resuming Relay Services $accountViewModel") + job = accountViewModel.justStart() + } + Lifecycle.Event.ON_PAUSE -> { + Log.d("ManageRelayServices", "Prepare to pause Relay Services $accountViewModel") + job?.cancel() + job = + scope.launch { + delay(30000) // 30 seconds + Log.d("ManageRelayServices", "Pausing Relay Services $accountViewModel") + accountViewModel.justPause() + } + } + else -> {} } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + job?.cancel() + lifeCycleOwner.lifecycle.removeObserver(observer) + Log.d("ManageRelayServices", "Disposing Relay Services $accountViewModel") + // immediately stops upon disposal + accountViewModel.pauseAndLogOff() } } } @@ -141,8 +167,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { job?.cancel() job = scope.launch { - val okHttpClient = HttpClientManager.getHttpClient(accountViewModel.account.shouldUseTorForTrustedRelays()) - PushNotificationUtils.checkAndInit(LocalPreferences.allSavedAccounts(), okHttpClient) + PushNotificationUtils.checkAndInit(LocalPreferences.allSavedAccounts(), accountViewModel::okHttpClientForTrustedRelays) } onPauseOrDispose { @@ -156,41 +181,19 @@ fun ManageTorInstance(accountViewModel: AccountViewModel) { val torSettings by accountViewModel.account.settings.torSettings.torType .collectAsStateWithLifecycle() if (torSettings == TorType.INTERNAL) { - WatchConnection(accountViewModel) - ManageTorInstanceInner(accountViewModel) + WatchTorConnection(accountViewModel) } } @Composable -fun ManageTorInstanceInner(accountViewModel: AccountViewModel) { - val context = LocalContext.current.applicationContext +fun WatchTorConnection(accountViewModel: AccountViewModel) { + val status by Amethyst.instance.torManager.status + .collectAsStateWithLifecycle() - val scope = rememberCoroutineScope() - var job = remember { null } - - LifecycleResumeEffect(key1 = accountViewModel) { - job?.cancel() - job = null - TorManager.startTorIfNotAlreadyOn(context) - - onPauseOrDispose { - job = - scope.launch { - delay(30000) // 5 seconds - TorManager.stopTor(context) - } - } - } -} - -@Composable -fun WatchConnection(accountViewModel: AccountViewModel) { - val status by TorManager.status.collectAsStateWithLifecycle() - - LaunchedEffect(key1 = status, key2 = accountViewModel) { - if (status is TorStatus.Active) { - Log.d("ServiceManager", "Tor is Active, force restart connections") - accountViewModel.changeProxyPort((status as TorStatus.Active).port) + if (status is TorServiceStatus.Active) { + LaunchedEffect(key1 = status, key2 = accountViewModel) { + Log.d("TorService", "Tor has just finished connecting, force restart relays $accountViewModel") + accountViewModel.changeProxyPort((status as TorServiceStatus.Active).port) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt index 32fdcb8c2..fc0de62ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -28,13 +28,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.INav @@ -61,7 +61,7 @@ fun RenderEncryptedFile( val mimeType = noteEvent.mimeType() if (algo == AESGCM.NAME && key != null && nonce != null) { - HttpClientManager.addCipherToCache(noteEvent.content, AESGCM(key, nonce), mimeType) + Amethyst.instance.keyCache.add(noteEvent.content, AESGCM(key, nonce), mimeType) val content by remember(noteEvent) { val isImage = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(noteEvent.content) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 9c71b4d24..2d0729965 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -655,7 +655,7 @@ class ChatNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( item.url.url, - accountViewModel?.account?.shouldUseTorForImageDownload() ?: false, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt index 441b38460..65a8e2eb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size +import okhttp3.OkHttpClient import java.util.Locale class IMetaAttachments { @@ -45,13 +46,13 @@ class IMetaAttachments { suspend fun downloadAndPrepare( url: String, - forceProxy: Boolean, + okHttpClient: (String) -> OkHttpClient, ) { val fileExtension: String = MimeTypeMap.getFileExtensionFromUrl(url) val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault())) val imeta = - FileHeader.prepare(url, mimeType, null, forceProxy).getOrNull()?.let { + FileHeader.prepare(url, mimeType, null, okHttpClient).getOrNull()?.let { IMetaTagBuilder(url) .apply { hash(it.hash) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 123ee4698..e78f36ff9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -77,6 +77,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage @@ -85,7 +86,6 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery @@ -599,7 +599,7 @@ fun ShowImageUploadGallery( val isImage = data.result.mimeTypeBeforeEncryption?.startsWith("image/") == true || RichTextParser.isImageUrl(data.result.url) if (data.cipher != null) { - HttpClientManager.addCipherToCache(data.result.url, data.cipher, data.result.mimeTypeBeforeEncryption) + Amethyst.instance.keyCache.add(data.result.url, data.cipher, data.result.mimeTypeBeforeEncryption) } val content by remember(data) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 85993cecd..6868bca4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache @@ -217,7 +218,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -230,7 +231,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - forceProxy = account::shouldUseTorForNIP96, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 4d624a49e..01d933ea7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -569,7 +569,7 @@ open class ChannelNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( item.url.url, - accountViewModel?.account?.shouldUseTorForImageDownload() ?: false, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload() ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt index 9e37a8387..8b9ae66fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt @@ -69,7 +69,7 @@ class TwoPaneNav( route: Route, upToClass: KClass, ) { - nav.popUpTo(route, upToClass) + nav.popUpTo(route, upToClass) } override fun closeDrawer() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index 76737f6c0..efbc5f58f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -51,7 +51,6 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser.Companion.isVideoUrl import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled @@ -292,7 +291,7 @@ fun UrlVideoView( callbackUri = content.uri, mimeType = content.mimeType, aspectRatio = ratio, - proxyPort = HttpClientManager.getCurrentProxyPort(accountViewModel.account.shouldUseTorForVideoDownload(content.url)), + proxyPort = accountViewModel.proxyPortFor(content.url), ) { mediaItem -> GetVideoController( mediaItem = mediaItem, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt index b2a3f2cf3..421b2369f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.Note import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.MutableStateFlow @Immutable data class ZapReqResponse( val zapRequest: Note, @@ -36,7 +36,7 @@ sealed class LnZapFeedState { object Loading : LnZapFeedState() class Loaded( - val feed: MutableState>, + val feed: MutableStateFlow>, ) : LnZapFeedState() object Empty : LnZapFeedState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt index 440e42123..3c237c5ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt @@ -73,13 +73,14 @@ private fun LnZapFeedLoaded( accountViewModel: AccountViewModel, nav: INav, ) { + val items by state.feed.collectAsStateWithLifecycle() val listState = rememberLazyListState() LazyColumn( contentPadding = FeedPadding, state = listState, ) { - itemsIndexed(state.feed.value, key = { _, item -> item.zapEvent.idHex }) { _, item -> + itemsIndexed(items, key = { _, item -> item.zapEvent.idHex }) { _, item -> ZapNoteCompose(item, accountViewModel = accountViewModel, nav = nav) HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt index 802e44863..7fffd030a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import android.util.Log import androidx.compose.runtime.Stable -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCache.notes import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists @@ -36,7 +36,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @Stable @@ -66,16 +65,14 @@ open class LnZapFeedViewModel( } private fun updateFeed(notes: ImmutableList) { - viewModelScope.launch(Dispatchers.Main) { - val currentState = _feedContent.value - if (notes.isEmpty()) { - _feedContent.update { LnZapFeedState.Empty } - } else if (currentState is LnZapFeedState.Loaded) { - // updates the current list - currentState.feed.value = notes - } else { - _feedContent.update { LnZapFeedState.Loaded(mutableStateOf(notes)) } - } + val currentState = _feedContent.value + if (notes.isEmpty()) { + _feedContent.tryEmit(LnZapFeedState.Empty) + } else if (currentState is LnZapFeedState.Loaded) { + // updates the current list + currentState.feed.tryEmit(notes) + } else { + _feedContent.tryEmit(LnZapFeedState.Loaded(MutableStateFlow(notes))) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 7764fda06..eb8e2f998 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace @@ -63,7 +64,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { _relays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( dirtyUrl = item.url, - forceProxy = account.shouldUseTorForDirty(item.url), + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, onInfo = { togglePaidRelay(item, it.limitation?.payment_required ?: false) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt index 348d5d87b..6b561f488 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3 import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace @@ -84,7 +85,7 @@ class Kind3RelayListViewModel : ViewModel() { _relays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( dirtyUrl = item.url, - forceProxy = account.shouldUseTorForDirty(item.url), + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, onInfo = { togglePaidRelay(item, it.limitation?.payment_required ?: false) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index 114da4bcd..628b9a73a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65 import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace @@ -84,7 +85,7 @@ class Nip65RelayListViewModel : ViewModel() { _homeRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( dirtyUrl = item.url, - forceProxy = account.shouldUseTorForDirty(item.url), + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, onInfo = { toggleHomePaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -95,7 +96,7 @@ class Nip65RelayListViewModel : ViewModel() { _notificationRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( dirtyUrl = item.url, - forceProxy = account.shouldUseTorForDirty(item.url), + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, onInfo = { toggleNotifPaidRelay(item, it.limitation?.payment_required ?: false) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 1d1d17749..83877c3d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -158,7 +158,7 @@ fun SecurityFiltersScreen( val pagerState = rememberPagerState { 3 } val coroutineScope = rememberCoroutineScope() var warnAboutReports by remember { mutableStateOf(accountViewModel.account.settings.syncedSettings.security.warnAboutPostsWithReports) } - var filterSpam by remember { mutableStateOf(accountViewModel.account.settings.syncedSettings.security.filterSpamFromStrangers) } + var filterSpam by remember { mutableStateOf(accountViewModel.account.settings.syncedSettings.security.filterSpamFromStrangers.value) } Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedState.kt index 03c287a6a..93eca9411 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedState.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings -import androidx.compose.runtime.MutableState import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.MutableStateFlow sealed class StringFeedState { object Loading : StringFeedState() class Loaded( - val feed: MutableState>, + val feed: MutableStateFlow>, ) : StringFeedState() object Empty : StringFeedState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt index a2026df8a..69704776a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt @@ -101,6 +101,7 @@ private fun FeedLoaded( post: (@Composable () -> Unit)? = null, inner: @Composable (String) -> Unit, ) { + val items by state.feed.collectAsStateWithLifecycle() val listState = rememberLazyListState() LazyColumn( @@ -109,7 +110,7 @@ private fun FeedLoaded( ) { item { pre?.let { it() } } - itemsIndexed(state.feed.value, key = { _, item -> item }) { _, item -> + itemsIndexed(items, key = { _, item -> item }) { _, item -> inner(item) HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt index 54746bf31..57885dca6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt @@ -41,7 +41,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class NostrHiddenWordsFeedViewModel( @@ -93,16 +92,14 @@ open class StringFeedViewModel( } private fun updateFeed(notes: ImmutableList) { - viewModelScope.launch(Dispatchers.Main) { - val currentState = _feedContent.value - if (notes.isEmpty()) { - _feedContent.update { StringFeedState.Empty } - } else if (currentState is StringFeedState.Loaded) { - // updates the current list - currentState.feed.value = notes - } else { - _feedContent.update { StringFeedState.Loaded(mutableStateOf(notes)) } - } + val currentState = _feedContent.value + if (notes.isEmpty()) { + _feedContent.tryEmit(StringFeedState.Empty) + } else if (currentState is StringFeedState.Loaded) { + // updates the current list + currentState.feed.tryEmit(notes) + } else { + _feedContent.tryEmit(StringFeedState.Loaded(MutableStateFlow(notes))) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt index 6e293478e..31773c6cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorManager.kt @@ -20,98 +20,41 @@ */ package com.vitorpamplona.amethyst.ui.tor -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder -import android.util.Log -import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE -import kotlinx.coroutines.flow.MutableStateFlow +import android.app.Application +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import org.torproject.jni.TorService -import org.torproject.jni.TorService.LocalBinder -import java.util.concurrent.atomic.AtomicBoolean -sealed class TorStatus { - data class Active( - val port: Int, - ) : TorStatus() - - object Off : TorStatus() - - object Connecting : TorStatus() -} - -object TorManager { - var runningIntent: Intent? = null - var torService: TorService? = null - - val status = MutableStateFlow(TorStatus.Off) - - // To make sure we don't start two services. - var isConnectingMutex = AtomicBoolean(false) - - fun startTorIfNotAlreadyOn(ctx: Context) { - if (runningIntent == null && isConnectingMutex.compareAndSet(false, true)) { - status.tryEmit(TorStatus.Connecting) - try { - startTor(ctx) - } finally { - isConnectingMutex.set(false) - } - } - } - - fun stopTor(ctx: Context) { - Log.d("TorManager", "Stopping Tor Service") - runningIntent?.let { - ctx.stopService(runningIntent) - } - status.tryEmit(TorStatus.Off) - runningIntent = null - torService = null - } - - private fun startTor(ctx: Context) { - Log.d("TorManager", "Binding Tor Service") - val currentIntent = Intent(ctx, TorService::class.java) - runningIntent = currentIntent - ctx.bindService( - currentIntent, - object : ServiceConnection { - override fun onServiceConnected( - name: ComponentName, - service: IBinder, - ) { - // moved torService to a local variable, since we only need it once - torService = (service as LocalBinder).service - - while (!isSocksReady()) { - try { - Thread.sleep(100) - } catch (e: InterruptedException) { - e.printStackTrace() - } - } - - status.tryEmit(TorStatus.Active(socksPort())) - Log.d("TorManager", "Tor Service Connected ${socksPort()}") - } - - override fun onServiceDisconnected(name: ComponentName) { - runningIntent = null - torService = null - status.tryEmit(TorStatus.Off) - Log.d("TorManager", "Tor Service Disconected") - } - }, - BIND_AUTO_CREATE, +/** + * There should be only one instance of the Tor binding per app. + * + * Tor will connect as soon as status is listened to. + */ +class TorManager( + app: Application, + scope: CoroutineScope, +) { + val status: StateFlow = + TorService(app).status.stateIn( + scope, + SharingStarted.WhileSubscribed(30000), + TorServiceStatus.Off, ) - } - fun isSocksReady() = torService?.let { it.socksPort > 0 } == true + val activePortOrNull: StateFlow = + status + .map { + (status.value as? TorServiceStatus.Active)?.port + }.stateIn( + scope, + SharingStarted.WhileSubscribed(2000), + (status.value as? TorServiceStatus.Active)?.port, + ) - fun socksPort(): Int = torService?.socksPort ?: 9050 + fun isSocksReady() = status.value is TorServiceStatus.Active - fun httpPort(): Int = torService?.httpTunnelPort ?: 9050 + fun socksPort(): Int = (status.value as? TorServiceStatus.Active)?.port ?: 9050 } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt new file mode 100644 index 000000000..024b1b085 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.tor + +import android.content.ComponentName +import android.content.Context +import android.content.Context.BIND_AUTO_CREATE +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.util.Log +import com.vitorpamplona.amethyst.service.checkNotInMainThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import org.torproject.jni.TorService +import org.torproject.jni.TorService.LocalBinder + +class TorService( + val context: Context, +) { + val status = + callbackFlow { + Log.d("TorService", "Binding Tor Service") + trySend(TorServiceStatus.Connecting) + + val currentIntent = Intent(context, TorService::class.java) + + context.bindService( + currentIntent, + object : ServiceConnection { + override fun onServiceConnected( + name: ComponentName, + service: IBinder, + ) { + launch { + // moved torService to a local variable, since we only need it once + val torService = (service as LocalBinder).service + + while (torService.socksPort < 0) { + delay(100) + } + + trySend(TorServiceStatus.Active(torService.socksPort)) + Log.d("TorService", "Tor Service Connected ${torService.socksPort}") + } + } + + override fun onServiceDisconnected(name: ComponentName) { + Log.d("TorService", "Tor Service Disconnected") + trySend(TorServiceStatus.Off) + } + }, + BIND_AUTO_CREATE, + ) + + awaitClose { + checkNotInMainThread() + Log.d("TorService", "Stopping Tor Service") + launch { + context.stopService(currentIntent) + } + trySend(TorServiceStatus.Off) + } + }.distinctUntilChanged().flowOn(Dispatchers.IO) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt new file mode 100644 index 000000000..cf3912319 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorServiceStatus.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.tor + +sealed class TorServiceStatus { + data class Active( + val port: Int, + ) : TorServiceStatus() + + object Off : TorServiceStatus() + + object Connecting : TorServiceStatus() +} diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt index 0a72ba21e..05da54353 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationReceiverService.kt @@ -26,11 +26,10 @@ import android.util.LruCache import androidx.core.content.ContextCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel -import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager -import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CoroutineScope @@ -38,7 +37,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import kotlin.time.measureTimedValue class PushNotificationReceiverService : FirebaseMessagingService() { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -46,11 +44,9 @@ class PushNotificationReceiverService : FirebaseMessagingService() { // this is called when a message is received override fun onMessageReceived(remoteMessage: RemoteMessage) { - Log.d("Time", "Notification received $remoteMessage") + Log.d("PushNotificationService", "Notification received $remoteMessage") scope.launch(Dispatchers.IO) { - val (value, elapsed) = - measureTimedValue { parseMessage(remoteMessage.data)?.let { receiveIfNew(it) } } - Log.d("Time", "Notification processed in $elapsed") + parseMessage(remoteMessage.data)?.let { receiveIfNew(it) } } } @@ -72,11 +68,11 @@ class PushNotificationReceiverService : FirebaseMessagingService() { override fun onCreate() { super.onCreate() - Log.d("Lifetime Event", "PushNotificationReceiverService.onCreate") + Log.d("PushNotificationService", "PushNotificationReceiverService.onCreate") } override fun onDestroy() { - Log.d("Lifetime Event", "PushNotificationReceiverService.onDestroy") + Log.d("PushNotificationService", "PushNotificationReceiverService.onDestroy") scope.cancel() super.onDestroy() @@ -84,10 +80,11 @@ class PushNotificationReceiverService : FirebaseMessagingService() { override fun onNewToken(token: String) { scope.launch(Dispatchers.IO) { - Log.d("Lifetime Event", "PushNotificationReceiverService.onNewToken") + Log.d("PushNotificationService", "PushNotificationReceiverService.onNewToken") // if the app is running, try to get tor. if not, goes open web. - val okHttpClient = HttpClientManager.getHttpClient(TorManager.isSocksReady()) - PushNotificationUtils.checkAndInit(token, LocalPreferences.allSavedAccounts(), okHttpClient) + PushNotificationUtils.checkAndInit(token, LocalPreferences.allSavedAccounts()) { + Amethyst.instance.okHttpClients.getHttpClient(Amethyst.instance.torManager.isSocksReady()) + } notificationManager().getOrCreateZapChannel(applicationContext) notificationManager().getOrCreateDMChannel(applicationContext) } diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt index 6b97f51e5..56ea9ce76 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/notifications/PushNotificationUtils.kt @@ -33,7 +33,7 @@ object PushNotificationUtils { suspend fun checkAndInit( accounts: List, - okHttpClient: OkHttpClient, + okHttpClient: (String) -> OkHttpClient, ) = with(Dispatchers.IO) { val token = FirebaseMessaging.getInstance().token.await() if (hasInit?.equals(accounts) == true && lastToken == token) { @@ -46,7 +46,7 @@ object PushNotificationUtils { suspend fun checkAndInit( token: String, accounts: List, - okHttpClient: OkHttpClient, + okHttpClient: (String) -> OkHttpClient, ) = with(Dispatchers.IO) { // initializes if the accounts are different or if the token has changed if (hasInit?.equals(accounts) == true && lastToken == token) { @@ -58,7 +58,7 @@ object PushNotificationUtils { private suspend fun registerToken( token: String, accounts: List, - okHttpClient: OkHttpClient, + okHttpClient: (String) -> OkHttpClient, ) = retryIfException("RegisterAccounts") { RegisterAccounts(accounts, okHttpClient).go(token) lastToken = token diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt index 70b61d5cc..2631384b5 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.ammolite.relays +import android.system.Os.close import android.util.Log +import androidx.core.app.PendingIntentCompat.send import com.vitorpamplona.ammolite.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.RelayState @@ -51,6 +53,11 @@ class NostrClient( fun getRelay(url: String): Relay? = relayPool.getRelay(url) + fun reconnect() { + // Reconnects all relays that may have disconnected + relayPool.requestAndWatch() + } + @Synchronized fun reconnect( relays: Array?, @@ -74,6 +81,9 @@ class NostrClient( relayPool.requestAndWatch() this.relays = newRelays.toTypedArray() } + } else { + // Reconnects all relays that may have disconnected + relayPool.requestAndWatch() } } else { if (this.relays.isNotEmpty()) { diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt index 2a6b27ad2..09ce54737 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt @@ -101,7 +101,7 @@ class Relay( val relaySubFilter = RelaySubFilter(url, activeTypes, subs) val inner = - SimpleClientRelay(url, socketBuilderFactory.build(forceProxy), relaySubFilter, this@Relay, RelayStats.get(url)) + SimpleClientRelay(url, socketBuilderFactory.build(url, forceProxy), relaySubFilter, this@Relay, RelayStats.get(url)) val brief = RelayBriefInfoCache.get(url) diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt new file mode 100644 index 000000000..3317cdd91 --- /dev/null +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.base64Image + +import android.R.attr.data +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import java.util.Base64 +import java.util.regex.Pattern + +class Base64Image { + companion object { + val pattern = Pattern.compile("data:image/(${RichTextParser.Companion.imageExtensions.joinToString(separator = "|") { it } });base64,([a-zA-Z0-9+/]+={0,2})") + + fun isBase64(content: String): Boolean { + val matcher = pattern.matcher(content) + return matcher.find() + } + + fun toBitmap(content: String): Bitmap { + val matcher = pattern.matcher(data.toString()) + + if (matcher.find()) { + val base64String = matcher.group(2) + + val byteArray = Base64.getDecoder().decode(base64String) + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + } + + throw Exception("Unable to convert base64 to image $content") + } + } +} diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 398e3eb5b..65cbdab2a 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -24,6 +24,7 @@ import android.util.Log import android.util.Patterns import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions +import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists @@ -103,11 +104,6 @@ class RichTextParser { } } - private fun checkBase64(content: String): Boolean { - val matcher = base64contentPattern.matcher(content) - return matcher.find() - } - fun parseValidUrls(content: String): LinkedHashSet { val urls = UrlDetector(content, UrlDetectorOptions.Default).detect() @@ -239,9 +235,7 @@ class RichTextParser { ): Segment { if (word.isEmpty()) return RegularTextSegment(word) - if (word.startsWith("data:image/")) { - if (checkBase64(word)) return Base64Segment(word) - } + if (word.startsWith("data:image/") && Base64Image.isBase64(word)) return Base64Segment(word) if (images.contains(word)) return ImageSegment(word) @@ -361,8 +355,6 @@ class RichTextParser { val imageExtensions = imageExt + imageExt.map { it.uppercase() } val videoExtensions = videoExt + videoExt.map { it.uppercase() } - val base64contentPattern = Pattern.compile("data:image/(${imageExtensions.joinToString(separator = "|") { it } });base64,([a-zA-Z0-9+/]+={0,2})") - val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)") val hashTagsPattern: Pattern = Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt index 7aa3095f8..e33bf3d6b 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt @@ -34,6 +34,8 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit class OtsTest { + val resolver = OtsResolver() + val otsEvent = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqCiiW0FlsU9lqK5f1A+cL6CGJ1Ah4V/A1yNJY/stUE3wECJz6ng/QxU5Z6xwaMx97qkI//AQqJv8bEMrGTplGWRv5qm4DgjxIHkcQqzpL0Fjr9VBAAijDe0IsQYpOhw1SIjZIgQa6i16CPEEZck7CvAIxR0AloJzCZoAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEOwPtjIkKI1hmtv9t1kuxZcI8QRlyTsK8Ahl0wrCSggZzgCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wEE1dVGa8JCuf2ek0c5ybDKII8SCBoVz8Sal45Kd1O8STWIGJTcl5JPtAZBZitqk3BE9MqAjxBGXJOwrwCHoGVgAZi9q9AIPf4w0u+QyOKShodHRwczovL2Zpbm5leS5jYWxlbmRhci5ldGVybml0eXdhbGwuY29t8BAFZFXFYg7DJJ0OzjmJ0FKWCPEEZck7CvAI0M49IcBR5bf/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPEgoE3IfYTmxxo4W/x/QYp/NGX6Wu93gSQkwbpjpOhZORcI8SDWLLurVQaXHdUuwivCfTfuxYCaq+AzypSGqLDAVocrEgjwIBfgjta16y13Gp4etQOCa9YiKEcM+/9AieG/vZolr3IDCPAgMR2zFCb384CEi8tVuI2fHgLT3I9zpe7oqJTzCcEqxWEI8SAJSdgeeosr7IxdOt8r7f0ipWc8FI6GAhgep8zSRgWikAjxIGxYmtCsC79Tx4z4YsT1WuMo+ycMkwhGQsQltF597cchCPAgCLrBf9vR1Aex6yY+vSkXAvLjMKdMqM/a1g8zNPwLeJcI8SBDCbTk4CczTuiIyZeUyYRVh31BZdjaSd2nU/pBQxu+6QjwIOwqE9/WqGC6CHH0i+tr7edvYX5PstSDf08KmnMqsqCoCPEgQIdEBg358vfek3Qjfyrgl51iCU6WUWmThsGLPDTcB0QI8SAX4dp64iI8pBx+zBqAQwUN6XgZ1cEfT8+2vha/9I1vzwjxWQEAAAAB8YJxvxJJth8OnxIV6UOXveIZAcJPTHcAkWgnucpuYqYAAAAAAP3///8CRhoMAAAAAAAWABTUUZK82uAvbU3vVyaaPIOddZBicwAAAAAAAAAAImog8ARCqgwACAjwIB9TcMDLhzgeS1Uw647lNvCfWECkkUvfrrOe6nay0sGdCAjwICYfs90sbPggoMICyOHGYbmOzop2L9mlnh4xqiBLY7yPCAjxINDiVWOBHnRmGJleQdB9myvJAJbNJ9kciZlTOkgJy89mCAjwIHfxqDLdwycj1Vtyth2CaSDdLQwiey9oV6Xov4stLpWNCAjxIGurJYpKJnKp9+y7MAdC+gXgHOiAu5P3RRUFW9l5hGaCCAjxICXqs9hdY0QMP/MNeqlt6s7xaIYtEXZ1CLvou5gaZNEICAjxIHdYxVeI76NXbT2zHcv6lw+v819Ooib7KWxc1GAsiX2fCAjwICPWdi3uBXOlIdmYi+V9C7wAqLyGE4DMoHD+GtvLizqiCAjwIOF2vENtWN5okEMMS+JSf1SGTY9yYP9j0JjXLbC1s+N1CAjxINWsgCtsPxhRNbe372k8/20WDbiL9e8934hGF256DvRECAjwII3mi+Li06j10ORxg0dYMkcsyGb115Jiqq1YEV3K/u+aCAgABYiWDXPXGQEDw9Qy\",\"created_at\":1707690688,\"id\":\"759f9da5846e936fab06766a524b36ba71c03bbc69ad0944fb8ee4bb1f3dd705\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"07c7896c8cbb97b5d7483097590c9d31b73f35c1ad9e752002bb5c1776cbd852e1d32704333d6930c9bc3e40f8b899a1f2e9f91cc3bf797d86acdecba7792576\",\"tags\":[[\"e\",\"a828a25b4165b14f65a8ae5fd40f9c2fa086275021e15fc0d7234963fb2d504d\"],[\"p\",\"595ca8eaace5899cb6ab7e2542bfc972136376f2eabc09287f1857eb8f167e53\"],[\"alt\",\"NIP-03 time stamp\"]]}" val otsEvent2 = "{\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwENyCNtiEN98IzIZgEu3cl6YI//AQ6TkSRd3BTGhDHCK1KkJc+AjxIAHaizG++NNL3Vm13BJrIhT7Br6tEYpb0TVRGaadgiUMCPAgOSDREH9v1Y50UHu79LfC4Lcd9WklQJzRQpw+Unb/pyII8QRltDD58AgqrxfAVrLw7QCD3+MNLvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQQMq/CLpGwY60nmddPS7OVgjxIDKxqd9nl+Mej41vP52Wd7gv7004r3n1rFGDObS8icRvCPAgH9TB/kwvXJEEw+h9Ce6fLaI3MORjtTEge0GbAefT6W4I8QRltDD58AhRcoU3gAo/swCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ//wECWtWsKo0uvSr8BYonjs3DEI8CBlsh2ng1Spl0K4oStYElGuMJsjd2uo5nXB+apo5A7ipwjxIM8oxynBwNA+QS/X7Ebtl1kyhFgfoOQioASNfCBzZ4gaCPEEZbQw+fAId6Yd5cw5gioAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb23wEJmPzXQbxv0AFTIyjTWjMskI8CAurbkrfrBtlinZXSDxj+m/oIkze57hGjTSxu1Xs87XYQjwIPk/LMD0zIgKoEE2dfeoYrrdHuO6dwmghTwUFajH2QzkCPEEZbQw+fAIE+Pq1/Wmdpj/AIPf4w0u+QyOIyJodHRwczovL2J0Yy5jYWxlbmRhci5jYXRhbGxheHkuY29tCPAgB2CbqkV7VpjRKIl3Ea6cBmB/EHcSN/YCgcc1E+mc07QI8CAfpkZ2Hh4Rukz3x4il3tZqQtlDlbna+I6so2t2YSEmMQjwIJOv32jbsMa2HJwpleRCKLEhgYOoHCSfpv1ZO0YNNNFsCPAgLMM7eFfCjokQfU4gdU5WpG/wBLkO9lDRF0GktL6ujt8I8SCRxJ0bC1PQ8qFmI/1jh8AS5d1/6VRJNMt1Hz41QmNr3QjwIBmgrKBF+OZ3y+XOMv2E7IZ4WwLr2u2H+ehsBfy7cPlICPEgm4ZMCSXzZVWu40d+zk2edaur6KOauo8X7V2KaFBR1VoI8SBKVVOiyq6IFqGn/15kLwk7L8upMAIZ0znjhYxYqSTQCQjwIMBD1twPZ33GxbwTiuOCeJPkoP++6R2wYpCii8UBTdgwCPAg84VkgMXwrt2xxRoeC1/6CtsFctki+w3m8Rs5/6g/IhEI8CCnzZQDhJyicX7bS7U8PMUObuC9Y4TXe+4THoXBMMXkxwjxIMZ0oAvshpcwowR3qPEDbwKZ6B4NPSU4Hz/+4PnD74gnCPAgIfLZEKqAvkMNXfakXoNq1UVqGSzL4Z86z5GzUfbvw8UI8SC8KoIeLvjd4vJ/xhNVphakPRd80YKeNkeYEuVH8k2EtAjwIPRtinLLxzt8iuw0XZtpTDzEstZOTNYVm+Bi3fEzdeIuCPFZAQAAAAE8vsasINN5DKon0KakX2HNdCB126ZLKXrw4PfvyEfqbwAAAAAA/f///wLPGw8AAAAAABYAFCvgxlh6msa4ZOtgvlc5KiZCx7IvAAAAAAAAAAAiaiDwBKygDAAICPEgB/2DJ3s6gMky/PceGZocTFRXjZUiCCAhHGYwQk/8yrUICPEgDuSd6+PJHUMuEHyLcKFxw7xfvRHRfInjkV3/Zy3BxqAICPEgZDgQ+4VXzlOIkGoO8EVxDgs2cWaeh4EEiaqa/y50gKAICPAgFI+zIuYcMF69GmPQVsXa9oy8eng7MeRZdIxArQyeX3oICPEgX5HYIuImpiSTEapgEssEW4l+W+4aRfNCG3pZf7z0hCoICPEgPkAbOSjFdtS4NT7MXgMYVQoQhI1JZtdFxUu4J3NTt7IICPEguW3qyuyGjctu5d9rM9P9ZCs/ZK4vAc+z21b9ygklgWAICPEgu4e2645xtvGhI1Zzuiv23vRhwE8uC9vj1TAgNg/C8UcICPAgwoQX8X0nY4HoQLRsJ0z8JCWQDzRh2iL2QXEb8z3gbjIICPAgzTwlPRtStsLJWhz3Q/0l8tMnrPSHVuh+zCiGk95dW2MICPAgGcBEuYZyzFNapHOfnJ9Q515QzO2VbIRhlVI0vIhd4jwICPAgidRMoM2pA+KmVJenVrLcbollsbUg9lL9bmv1C1dSxswICPAgZinGakwhbHdanTaRJeBkEUlbhfNokvj8b5KneyG+wzIICAAFiJYNc9cZAQOtwTI=\",\"created_at\":1706324334,\"id\":\"2ad074ddb7724eb13b4244b49cf2321b1057f37fdf8ce102e6329b839cf763a9\",\"kind\":1040,\"pubkey\":\"82fbb08c8ef45c4d71c88368d0ae805bc62fb92f166ab04a0b7a0c83d8cbc29a\",\"sig\":\"ad7274bb32ba9e9cfdbd52f4887e8a2fda1047c75a7185b2ab7ff254ebac14ed48a2b60737494d655e24c9400eeeec7e29293a77bfcaafaecd94b350c9a2c22b\",\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"p\",\"c31e22c3715c1bde5608b7e0d04904f22f5fc453ba1806d21c9f2382e1e58c6c\"],[\"alt\",\"NIP-03 time stamp\"]]}" val otsPendingEvent = "{\"id\":\"12fa15ad4b4cf9dc5940389325b69b93c5c1f59c049c701ee669b275299fdaf1\",\"pubkey\":\"dcaa6c8a2f47b6fef4a34b20e8843c59dbe7c5f07a402338c09fd147dd01d22b\",\"created_at\":1708877521,\"kind\":1040,\"tags\":[[\"e\",\"a8634f5368e17789e8fb3836cad0e4b9fe4f2288afafc28723c04bebc68a18d6\"],[\"alt\",\"Opentimestamps Attestation\"]],\"content\":\"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIqGNPU2jhd4no+zg2ytDkuf5PIoivr8KHI8BL68aKGNbwELidvzr0usf55CkpKf6OABQI//AQK3sWd2tq+7KO8YNJIARJugjxBGXbZtPwCL0H4/7GL5+SAIPf4w0u+QyOLCtodHRwczovL2JvYi5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AQPDZsJgN1TnJXoUzlsgo93wjwIIfBc7LUqkCbC1BLZRZ+6LXztK50UdH5xe7fn40bupkrCPEEZdtm0/AI0CADXN5ZIncAg9/jDS75DI4uLWh0dHBzOi8vYWxpY2UuYnRjLmNhbGVuZGFyLm9wZW50aW1lc3RhbXBzLm9yZ/AQcELcSrE04cuGKlZQf2LeVwjwILUDSf9vK2GaefKTpn/LV2oUsQaA5WbqaP3C+1ZxQfRNCPEEZdtm0/AIbCtb+yRXFqUAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNhbGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=\",\"sig\":\"f6854c0228c15c08aeb70bbabe9ed87bbb7289fab31b13cabac15138bb71179553e06080b83f4a813fbdaf614f63293beea3fc73fe865da6551193fa4d38de04\"}" @@ -43,22 +45,22 @@ class OtsTest { @Test fun verifyNostrEvent() { val ots = Event.fromJson(otsEvent) as OtsEvent - println(OtsResolver.info(ots.otsByteArray())) - assertEquals(1707688818L, ots.verify()) + println(resolver.info(ots.otsByteArray())) + assertEquals(1707688818L, ots.verify(resolver)) } @Test fun verifyNostrEvent2() { val ots = Event.Companion.fromJson(otsEvent2) as OtsEvent - println(OtsResolver.info(ots.otsByteArray())) - assertEquals(1706322179L, ots.verify()) + println(resolver.info(ots.otsByteArray())) + assertEquals(1706322179L, ots.verify(resolver)) } @Test fun verifyNostrPendingEvent() { val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent - println(OtsResolver.info(ots.otsByteArray())) - assertEquals(null, ots.verify()) + println(resolver.info(ots.otsByteArray())) + assertEquals(null, ots.verify(resolver)) val eventId = ots.digestEventId() ?: run { @@ -66,7 +68,7 @@ class OtsTest { return } - val upgraded = OtsEvent.upgrade(ots.otsByteArray(), eventId) + val upgraded = OtsEvent.upgrade(ots.otsByteArray(), eventId, resolver) assertNotNull(upgraded) @@ -83,9 +85,9 @@ class OtsTest { Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) println(newOts!!.toJson()) - println(OtsResolver.info(newOts!!.otsByteArray())) + println(resolver.info(newOts.otsByteArray())) - assertEquals(1708879025L, newOts!!.verify()) + assertEquals(1708879025L, newOts.verify(resolver)) } @Test @@ -95,7 +97,7 @@ class OtsTest { val countDownLatch = CountDownLatch(1) - signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest))) { + signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest, resolver))) { ots = it countDownLatch.countDown() } @@ -103,8 +105,8 @@ class OtsTest { Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) println(ots!!.toJson()) - println(OtsResolver.info(ots!!.otsByteArray())) + println(resolver.info(ots.otsByteArray())) - assertEquals(null, ots!!.verify()) + assertEquals(null, ots.verify(resolver)) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilderFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilderFactory.kt index 1cbcdddd9..5be40adee 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilderFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilderFactory.kt @@ -21,5 +21,8 @@ package com.vitorpamplona.quartz.nip01Core.relay.sockets interface WebsocketBuilderFactory { - fun build(forceProxy: Boolean): WebsocketBuilder + fun build( + url: String, + forceProxy: Boolean, + ): WebsocketBuilder } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt index bcd82b3fb..d6f236275 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt @@ -51,27 +51,30 @@ class OtsEvent( fun otsByteArray(): ByteArray = decodeOtsState(content) - fun cacheVerify(): VerificationState = VerificationStateCache.cacheVerify(this) + fun verifyState(resolver: OtsResolver): VerificationState = digestEventId()?.let { verify(otsByteArray(), it, resolver) } ?: VerificationState.Error("Digest Not found") - fun verifyState(): VerificationState = digestEventId()?.let { verify(otsByteArray(), it) } ?: VerificationState.Error("Digest Not found") - - fun verify(): Long? = (verifyState() as? VerificationState.Verified)?.verifiedTime + fun verify(resolver: OtsResolver): Long? = (verifyState(resolver) as? VerificationState.Verified)?.verifiedTime companion object { const val KIND = 1040 const val ALT = "Opentimestamps Attestation" - fun stamp(eventId: HexKey) = OtsResolver.stamp(eventId.hexToByteArray()) + fun stamp( + eventId: HexKey, + resolver: OtsResolver, + ) = resolver.stamp(eventId.hexToByteArray()) fun upgrade( otsState: ByteArray, eventId: HexKey, - ) = OtsResolver.upgrade(otsState, eventId.hexToByteArray()) + resolver: OtsResolver, + ) = resolver.upgrade(otsState, eventId.hexToByteArray()) fun verify( otsState: ByteArray, eventId: HexKey, - ) = OtsResolver.verify(otsState, eventId.hexToByteArray()) + resolver: OtsResolver, + ) = resolver.verify(otsState, eventId.hexToByteArray()) fun encodeOtsState(otsState: ByteArray) = Base64.getEncoder().encodeToString(otsState) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt index 384c46e59..e7bff90eb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolver.kt @@ -21,7 +21,9 @@ package com.vitorpamplona.quartz.nip03Timestamp import android.util.Log +import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockstreamExplorer +import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarBuilder import com.vitorpamplona.quartz.nip03Timestamp.ots.CalendarPureJavaBuilder import com.vitorpamplona.quartz.nip03Timestamp.ots.DetachedTimestampFile import com.vitorpamplona.quartz.nip03Timestamp.ots.Hash @@ -31,13 +33,11 @@ import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException import com.vitorpamplona.quartz.nip03Timestamp.ots.op.OpSHA256 import kotlinx.coroutines.CancellationException -object OtsResolver { - // default config - var ots = - OpenTimestamps( - BlockstreamExplorer(), - CalendarPureJavaBuilder(), - ) +class OtsResolver( + explorer: BitcoinExplorer = BlockstreamExplorer(), + calendarBuilder: CalendarBuilder = CalendarPureJavaBuilder(), +) { + val ots: OpenTimestamps = OpenTimestamps(explorer, calendarBuilder) fun info(otsState: ByteArray): String = ots.info(DetachedTimestampFile.deserialize(otsState)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt index 98d7ce76a..9c31d011f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt @@ -24,21 +24,24 @@ import android.util.LruCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.TimeUtils -object VerificationStateCache { +class VerificationStateCache { private val cache = LruCache(200) - fun cacheVerify(event: OtsEvent): VerificationState = + fun cacheVerify( + event: OtsEvent, + resolverBuilder: () -> OtsResolver, + ): VerificationState = when (val verif = cache[event.id]) { is VerificationState.Verified -> verif is VerificationState.NetworkError -> { // try again in 5 mins if (verif.time < TimeUtils.fiveMinutesAgo()) { - event.verifyState().also { cache.put(event.id, it) } + event.verifyState(resolverBuilder()).also { cache.put(event.id, it) } } else { verif } } is VerificationState.Error -> verif - else -> event.verifyState().also { cache.put(event.id, it) } + else -> event.verifyState(resolverBuilder()).also { cache.put(event.id, it) } } }