diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c8916197..e14378264 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,8 +37,12 @@ android { applicationId = "dev.dimension.flare" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.compileSdk.get().toInt() - versionCode = System.getenv("BUILD_NUMBER")?.toIntOrNull() ?: fdroidProp.getProperty("versionCode")?.toIntOrNull() ?: 1 - versionName = System.getenv("BUILD_VERSION")?.toString() ?: fdroidProp.getProperty("versionName")?.toString() ?: "0.0.0" + versionCode = + System.getenv("BUILD_NUMBER")?.toIntOrNull() ?: fdroidProp.getProperty("versionCode") + ?.toIntOrNull() ?: 1 + versionName = + System.getenv("BUILD_VERSION")?.toString() ?: fdroidProp.getProperty("versionName") + ?.toString() ?: "0.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -188,3 +192,57 @@ if (project.file("google-services.json").exists()) { uploadCrashlyticsMappingFileRelease.dependsOn(processDebugGoogleServices) } } + + +abstract class GenerateDeepLinkManifestTask : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val hostsFile: RegularFileProperty + @get:OutputFile + abstract val manifest: RegularFileProperty + + @TaskAction + fun run() { + val hosts = hostsFile.get().asFile.readLines() + .map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("#") } + .distinct() + val dataTags = hosts.joinToString("\n") { host -> + """""" + } + + manifest.get().asFile.writeText( + """ + + + + + + + + + + $dataTags + + + + + """.trimIndent() + ) + } +} + +extensions.getByType(com.android.build.api.variant.AndroidComponentsExtension::class.java) + .onVariants { variant: com.android.build.api.variant.Variant -> + val t = tasks.register( + "generate${variant.name.replaceFirstChar { it.uppercase() }}DeepLinkManifest", + GenerateDeepLinkManifestTask::class.java + ) { + hostsFile = project.layout.projectDirectory.file("deeplink.txt") + } + + variant.sources.manifests.addGeneratedManifestFile( + t, + GenerateDeepLinkManifestTask::manifest + ) + } diff --git a/app/deeplink.txt b/app/deeplink.txt new file mode 100644 index 000000000..0e61ddef0 --- /dev/null +++ b/app/deeplink.txt @@ -0,0 +1,24 @@ +x.com +twitter.com +bsky.app +pawoo.net +mastodon.social +misskey.io +next.misskey.io +mstdn.jp +mstdn.social +mastodon.world +mastodon.sdf.org +universeodon.com +techhub.social +mastodonapp.uk +mastodon.uno +m.cmx.im +mstdn.party +infosec.exchange +hachyderm.io +sfba.social +misskey.design +ohai.social +mastodon.nl +troet.cafe \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 539077157..9b558b21c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ @@ -41,17 +41,6 @@ - - - - - - - - - - - diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt index 3b526e569..050ab61a0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt @@ -50,6 +50,21 @@ internal object DeepLinkMapping { statusKey = MicroBlogKey(id, accountKey.host), ) } + + @Serializable + data class PostMedia( + val handle: String, + val id: String, + val index: Int, + ) : Type { + override fun deepLink(accountKey: MicroBlogKey): DeeplinkRoute = + DeeplinkRoute.Media.StatusMedia( + accountType = AccountType.Specific(accountKey), + statusKey = MicroBlogKey(id, accountKey.host), + index = index, + preview = null, + ) + } } fun generatePattern( @@ -111,6 +126,13 @@ internal object DeepLinkMapping { "https://www.$xqtHost/{handle}/status/{id}", "https://www.$xqtOldHost/{handle}/", ) + val media = + listOf( + "https://$xqtHost/{handle}/status/{id}/photo/{index}", + "https://$xqtOldHost/{handle}/status/{id}/photo/{index}", + "https://www.$xqtHost/{handle}/status/{id}/photo/{index}", + "https://www.$xqtOldHost/{handle}/status/{id}/photo/{index}", + ) profile.map { DeepLinkPattern( Type.Profile.serializer(), @@ -122,6 +144,12 @@ internal object DeepLinkMapping { Type.Post.serializer(), Url(it), ) + } + + media.map { + DeepLinkPattern( + Type.PostMedia.serializer(), + Url(it), + ) } } diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt index 0383cdc37..9d1ffcf56 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt @@ -29,7 +29,9 @@ class DeepLinkMappingTest { assertEquals(Url("https://$host/@{handle}"), profile.uriPattern) assertEquals( listOf("handle" to true), - profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + profile.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) val post = patterns[1] @@ -37,7 +39,9 @@ class DeepLinkMappingTest { assertEquals(Url("https://$host/@{handle}/{id}"), post.uriPattern) assertEquals( listOf("handle" to true, "id" to true), - post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + post.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) } @@ -54,7 +58,9 @@ class DeepLinkMappingTest { assertEquals(Url("https://$host/@{handle}"), profile.uriPattern) assertEquals( listOf("handle" to true), - profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + profile.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) val post = patterns[1] @@ -62,7 +68,9 @@ class DeepLinkMappingTest { assertEquals(Url("https://$host/notes/{id}"), post.uriPattern) assertEquals( listOf("notes" to false, "id" to true), - post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + post.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) } @@ -79,7 +87,9 @@ class DeepLinkMappingTest { assertEquals(Url("https://$host/profile/{handle}"), profile.uriPattern) assertEquals( listOf("profile" to false, "handle" to true), - profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + profile.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) val post = patterns[1] @@ -87,7 +97,9 @@ class DeepLinkMappingTest { assertEquals(Url("https://$host/profile/{handle}/post/{id}"), post.uriPattern) assertEquals( listOf("profile" to false, "handle" to true, "post" to false, "id" to true), - post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + post.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) } @@ -97,14 +109,16 @@ class DeepLinkMappingTest { val patterns = DeepLinkMapping.generatePattern(PlatformType.xQt, host) - assertEquals(8, patterns.size) + assertEquals(12, patterns.size) val profile = patterns[0] assertEquals(DeepLinkMapping.Type.Profile.serializer(), profile.serializer) assertEquals(Url("https://$host/{handle}"), profile.uriPattern) assertEquals( listOf("handle" to true), - profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + profile.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) val post = patterns[4] @@ -112,7 +126,25 @@ class DeepLinkMappingTest { assertEquals(Url("https://$host/{handle}/status/{id}"), post.uriPattern) assertEquals( listOf("handle" to true, "status" to false, "id" to true), - post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg }, + post.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, + ) + + val media = patterns[8] + assertEquals(DeepLinkMapping.Type.PostMedia.serializer(), media.serializer) + assertEquals(Url("https://$host/{handle}/status/{id}/photo/{index}"), media.uriPattern) + assertEquals( + listOf( + "handle" to true, + "status" to false, + "id" to true, + "photo" to false, + "index" to true, + ), + media.pathSegments + .filter { it.stringValue.isNotEmpty() } + .map { it.stringValue to it.isParamArg }, ) } @@ -172,8 +204,18 @@ class DeepLinkMappingTest { val mapping: ImmutableMap>> = persistentMapOf( - account1 to DeepLinkMapping.generatePattern(PlatformType.Mastodon, account1.accountKey.host).toImmutableList(), - account2 to DeepLinkMapping.generatePattern(PlatformType.Mastodon, account2.accountKey.host).toImmutableList(), + account1 to + DeepLinkMapping + .generatePattern( + PlatformType.Mastodon, + account1.accountKey.host, + ).toImmutableList(), + account2 to + DeepLinkMapping + .generatePattern( + PlatformType.Mastodon, + account2.accountKey.host, + ).toImmutableList(), ) val matches = DeepLinkMapping.matches("https://mastodon.social/@alice", mapping) @@ -193,7 +235,12 @@ class DeepLinkMappingTest { val mapping: ImmutableMap>> = persistentMapOf( - account to DeepLinkMapping.generatePattern(PlatformType.Mastodon, account.accountKey.host).toImmutableList(), + account to + DeepLinkMapping + .generatePattern( + PlatformType.Mastodon, + account.accountKey.host, + ).toImmutableList(), ) // URL containing none of the valid hosts @@ -239,33 +286,60 @@ class DeepLinkMappingTest { PlatformType.Misskey, misskeyAccount.accountKey.host, ).toImmutableList(), - bskyAccount to DeepLinkMapping.generatePattern(PlatformType.Bluesky, bskyAccount.accountKey.host).toImmutableList(), - xAccount to DeepLinkMapping.generatePattern(PlatformType.xQt, xAccount.accountKey.host).toImmutableList(), + bskyAccount to + DeepLinkMapping + .generatePattern( + PlatformType.Bluesky, + bskyAccount.accountKey.host, + ).toImmutableList(), + xAccount to + DeepLinkMapping + .generatePattern( + PlatformType.xQt, + xAccount.accountKey.host, + ).toImmutableList(), ) // https://mastodon.example/@alice - val mastodonProfileMatch = DeepLinkMapping.matches("https://mastodon.example/@alice", mapping) + val mastodonProfileMatch = + DeepLinkMapping.matches("https://mastodon.example/@alice", mapping) assertEquals(DeepLinkMapping.Type.Profile("alice"), mastodonProfileMatch[mastodonAccount]) // https://mastodon.example/@alice/12345 - val mastodonPostMatch = DeepLinkMapping.matches("https://mastodon.example/@alice/12345", mapping) - assertEquals(DeepLinkMapping.Type.Post("alice", "12345"), mastodonPostMatch[mastodonAccount]) + val mastodonPostMatch = + DeepLinkMapping.matches("https://mastodon.example/@alice/12345", mapping) + assertEquals( + DeepLinkMapping.Type.Post("alice", "12345"), + mastodonPostMatch[mastodonAccount], + ) // https://misskey.example/@bob val misskeyProfileMatch = DeepLinkMapping.matches("https://misskey.example/@bob", mapping) assertEquals(DeepLinkMapping.Type.Profile("bob"), misskeyProfileMatch[misskeyAccount]) // https://misskey.example/notes/12345 - val misskeyPostMatch = DeepLinkMapping.matches("https://misskey.example/notes/12345", mapping) + val misskeyPostMatch = + DeepLinkMapping.matches("https://misskey.example/notes/12345", mapping) assertEquals(DeepLinkMapping.Type.Post(null, "12345"), misskeyPostMatch[misskeyAccount]) // https://bsky.example/profile/alice.bsky.social - val bskyProfileMatch = DeepLinkMapping.matches("https://bsky.example/profile/alice.bsky.social", mapping) - assertEquals(DeepLinkMapping.Type.Profile("alice.bsky.social"), bskyProfileMatch[bskyAccount]) + val bskyProfileMatch = + DeepLinkMapping.matches("https://bsky.example/profile/alice.bsky.social", mapping) + assertEquals( + DeepLinkMapping.Type.Profile("alice.bsky.social"), + bskyProfileMatch[bskyAccount], + ) // https://bsky.example/profile/alice.bsky.social/post/12345 - val bskyPostMatch = DeepLinkMapping.matches("https://bsky.example/profile/alice.bsky.social/post/12345", mapping) - assertEquals(DeepLinkMapping.Type.Post("alice.bsky.social", "12345"), bskyPostMatch[bskyAccount]) + val bskyPostMatch = + DeepLinkMapping.matches( + "https://bsky.example/profile/alice.bsky.social/post/12345", + mapping, + ) + assertEquals( + DeepLinkMapping.Type.Post("alice.bsky.social", "12345"), + bskyPostMatch[bskyAccount], + ) // https://x.example/alice val xProfileMatch = DeepLinkMapping.matches("https://$xqtHost/alice", mapping) @@ -274,6 +348,11 @@ class DeepLinkMappingTest { // https://x.example/alice/status/12345 val xPostMatch = DeepLinkMapping.matches("https://$xqtHost/alice/status/12345", mapping) assertEquals(DeepLinkMapping.Type.Post("alice", "12345"), xPostMatch[xAccount]) + + // https://x.example/alice/status/12345/photo/1 + val xPostPhotoMatch = + DeepLinkMapping.matches("https://$xqtHost/alice/status/12345/photo/1", mapping) + assertEquals(DeepLinkMapping.Type.PostMedia("alice", "12345", 1), xPostPhotoMatch[xAccount]) } @Test