Skip to content

Commit d57b7f8

Browse files
authored
Merge pull request #1620 from DimensionDev/feature/deeplink
deeplink support
2 parents 090f988 + 4eef30f commit d57b7f8

File tree

42 files changed

+1814
-509
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1814
-509
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
77
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
88
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
9-
9+
<queries>
10+
<intent>
11+
<action android:name="android.intent.action.VIEW" />
12+
<category android:name="android.intent.category.BROWSABLE" />
13+
<data android:scheme="https" />
14+
</intent>
15+
</queries>
1016
<application
1117
android:allowBackup="true"
1218
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -32,6 +38,16 @@
3238
<intent-filter>
3339
<data android:scheme="flare" />
3440
<action android:name="android.intent.action.VIEW" />
41+
<category android:name="android.intent.category.DEFAULT" />
42+
<category android:name="android.intent.category.BROWSABLE" />
43+
</intent-filter>
44+
<intent-filter android:autoVerify="false">
45+
<action android:name="android.intent.action.VIEW" />
46+
<data android:scheme="https" />
47+
<data android:host="x.com" />
48+
<data android:host="twitter.com" />
49+
<data android:host="www.x.com" />
50+
<data android:host="www.twitter.com" />
3551

3652
<category android:name="android.intent.category.DEFAULT" />
3753
<category android:name="android.intent.category.BROWSABLE" />

app/src/main/java/dev/dimension/flare/ui/AppContainer.kt

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package dev.dimension.flare.ui
22

3+
import android.content.Context
4+
import android.content.Intent
5+
import android.content.pm.PackageManager
36
import androidx.browser.customtabs.CustomTabsIntent
47
import androidx.compose.runtime.Composable
58
import androidx.compose.runtime.CompositionLocalProvider
@@ -42,24 +45,16 @@ fun FlareApp(content: @Composable () -> Unit) {
4245
val originalUriHandler = LocalUriHandler.current
4346
val context = LocalContext.current
4447
val uriHandler =
45-
if (appearanceSettings.inAppBrowser) {
46-
remember {
47-
object : UriHandler {
48-
override fun openUri(uri: String) {
49-
if (uri.startsWith("http://") || uri.startsWith("https://")) {
50-
val intent =
51-
CustomTabsIntent
52-
.Builder()
53-
.build()
54-
intent.launchUrl(context, uri.toUri())
55-
} else {
56-
originalUriHandler.openUri(uri)
57-
}
48+
remember(appearanceSettings.inAppBrowser) {
49+
object : UriHandler {
50+
override fun openUri(uri: String) {
51+
if (uri.startsWith("http://") || uri.startsWith("https://")) {
52+
openInBrowser(context, uri, appearanceSettings.inAppBrowser)
53+
} else {
54+
originalUriHandler.openUri(uri)
5855
}
5956
}
6057
}
61-
} else {
62-
originalUriHandler
6358
}
6459

6560
val appSettings by settingsRepository.appSettings.collectAsState(AppSettings(""))
@@ -99,3 +94,46 @@ fun FlareApp(content: @Composable () -> Unit) {
9994
content = content,
10095
)
10196
}
97+
98+
private fun getNonSelfBrowserPackageName(
99+
context: Context,
100+
url: String,
101+
): String? {
102+
val packageName = context.packageName
103+
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
104+
val packageManager = context.packageManager
105+
val list = packageManager.queryIntentActivities(browserIntent, PackageManager.MATCH_ALL)
106+
val defaultResolveInfo = packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
107+
if (defaultResolveInfo != null && defaultResolveInfo.activityInfo.packageName != packageName) {
108+
return defaultResolveInfo.activityInfo.packageName
109+
}
110+
for (info in list) {
111+
if (info.activityInfo.packageName != packageName) {
112+
return info.activityInfo.packageName
113+
}
114+
}
115+
116+
return null
117+
}
118+
119+
private fun openInBrowser(
120+
context: Context,
121+
url: String,
122+
inAppBrowser: Boolean,
123+
) {
124+
val targetPackage = getNonSelfBrowserPackageName(context, url)
125+
if (targetPackage != null) {
126+
if (inAppBrowser) {
127+
val builder = CustomTabsIntent.Builder()
128+
val customTabsIntent = builder.build()
129+
customTabsIntent.intent.setPackage(targetPackage)
130+
customTabsIntent.launchUrl(context, url.toUri())
131+
} else {
132+
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
133+
intent.addCategory(Intent.CATEGORY_BROWSABLE)
134+
intent.setPackage(targetPackage)
135+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
136+
context.startActivity(intent)
137+
}
138+
}
139+
}

app/src/main/java/dev/dimension/flare/ui/common/OnNewIntent.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fun OnNewIntent(
2020
key1: Any? = null,
2121
key2: Any? = null,
2222
key3: Any? = null,
23+
withOnCreateIntent: Boolean = false,
2324
onNewIntent: (Intent) -> Unit,
2425
) {
2526
val context = LocalContext.current
@@ -30,6 +31,9 @@ fun OnNewIntent(
3031
onNewIntent(it)
3132
}
3233
activity.addOnNewIntentListener(listener)
34+
if (withOnCreateIntent) {
35+
onNewIntent.invoke(activity.intent)
36+
}
3337
onDispose { activity.removeOnNewIntentListener(listener) }
3438
}
3539
}

app/src/main/java/dev/dimension/flare/ui/common/ProxyUriHandler.kt

Lines changed: 0 additions & 19 deletions
This file was deleted.

app/src/main/java/dev/dimension/flare/ui/route/Route.kt

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import androidx.navigation3.runtime.NavKey
44
import dev.dimension.flare.data.model.TimelineTabItem
55
import dev.dimension.flare.model.AccountType
66
import dev.dimension.flare.model.MicroBlogKey
7+
import kotlinx.collections.immutable.ImmutableMap
8+
import kotlinx.collections.immutable.toImmutableMap
79
import kotlinx.serialization.Serializable
810

911
@Serializable
@@ -404,110 +406,151 @@ internal sealed interface Route : NavKey {
404406
@Serializable
405407
data object AccountSelection : Route
406408

409+
@Serializable
410+
data class DeepLinkAccountPicker(
411+
val originalUrl: String,
412+
val data: ImmutableMap<MicroBlogKey, Route>,
413+
) : Route
414+
407415
companion object {
408416
public fun parse(url: String): Route? {
409417
val deeplinkRoute = DeeplinkRoute.parse(url) ?: return null
418+
return from(deeplinkRoute)
419+
}
420+
421+
public fun from(deeplinkRoute: DeeplinkRoute): Route? {
410422
return when (deeplinkRoute) {
411-
is DeeplinkRoute.Login -> Route.ServiceSelect.Selection
412-
is DeeplinkRoute.Callback -> null
423+
is DeeplinkRoute.OpenLinkDirectly -> null
424+
is DeeplinkRoute.DeepLinkAccountPicker ->
425+
DeepLinkAccountPicker(
426+
originalUrl = deeplinkRoute.originalUrl,
427+
data =
428+
deeplinkRoute.data
429+
.mapNotNull { (key, value) ->
430+
val route = Route.from(value) ?: return@mapNotNull null
431+
key to route
432+
}.toMap()
433+
.toImmutableMap(),
434+
)
435+
436+
is DeeplinkRoute.Login -> ServiceSelect.Selection
413437
is DeeplinkRoute.Compose.New ->
414-
Route.Compose.New(accountType = deeplinkRoute.accountType)
438+
Compose.New(accountType = deeplinkRoute.accountType)
439+
415440
is DeeplinkRoute.Compose.Quote ->
416-
Route.Compose.Quote(
441+
Compose.Quote(
417442
accountKey = deeplinkRoute.accountKey,
418443
statusKey = deeplinkRoute.statusKey,
419444
)
445+
420446
is DeeplinkRoute.Compose.Reply ->
421-
Route.Compose.Reply(
447+
Compose.Reply(
422448
accountKey = deeplinkRoute.accountKey,
423449
statusKey = deeplinkRoute.statusKey,
424450
)
451+
425452
is DeeplinkRoute.Compose.VVOReplyComment ->
426-
Route.Compose.VVOReplyComment(
453+
Compose.VVOReplyComment(
427454
accountKey = deeplinkRoute.accountKey,
428455
replyTo = deeplinkRoute.replyTo,
429456
rootId = deeplinkRoute.rootId,
430457
)
458+
431459
is DeeplinkRoute.Media.Image ->
432-
Route.Media.Image(
460+
Media.Image(
433461
uri = deeplinkRoute.uri,
434462
previewUrl = deeplinkRoute.previewUrl,
435463
)
464+
436465
is DeeplinkRoute.Media.Podcast ->
437-
Route.Media.Podcast(
466+
Media.Podcast(
438467
accountType = deeplinkRoute.accountType,
439468
id = deeplinkRoute.id,
440469
)
470+
441471
is DeeplinkRoute.Media.StatusMedia ->
442-
Route.Media.StatusMedia(
472+
Media.StatusMedia(
443473
statusKey = deeplinkRoute.statusKey,
444474
accountType = deeplinkRoute.accountType,
445475
index = deeplinkRoute.index,
446476
preview = deeplinkRoute.preview,
447477
)
478+
448479
is DeeplinkRoute.Profile.User ->
449-
Route.Profile.User(
480+
Profile.User(
450481
accountType = deeplinkRoute.accountType,
451482
userKey = deeplinkRoute.userKey,
452483
)
484+
453485
is DeeplinkRoute.Profile.UserNameWithHost ->
454-
Route.Profile.UserNameWithHost(
486+
Profile.UserNameWithHost(
455487
accountType = deeplinkRoute.accountType,
456488
name = deeplinkRoute.userName,
457489
host = deeplinkRoute.host,
458490
)
491+
459492
is DeeplinkRoute.Rss.Detail ->
460-
Route.Rss.Detail(
493+
Rss.Detail(
461494
url = deeplinkRoute.url,
462495
)
496+
463497
is DeeplinkRoute.Search ->
464-
Route.Search(
498+
Search(
465499
accountType = deeplinkRoute.accountType,
466500
query = deeplinkRoute.query,
467501
)
502+
468503
is DeeplinkRoute.Status.AddReaction ->
469-
Route.Status.AddReaction(
504+
Status.AddReaction(
470505
statusKey = deeplinkRoute.statusKey,
471506
accountType = deeplinkRoute.accountType,
472507
)
508+
473509
is DeeplinkRoute.Status.AltText ->
474-
Route.Status.AltText(
510+
Status.AltText(
475511
text = deeplinkRoute.text,
476512
)
513+
477514
is DeeplinkRoute.Status.BlueskyReport ->
478-
Route.Status.BlueskyReport(
515+
Status.BlueskyReport(
479516
statusKey = deeplinkRoute.statusKey,
480517
accountType = deeplinkRoute.accountType,
481518
)
519+
482520
is DeeplinkRoute.Status.DeleteConfirm ->
483-
Route.Status.DeleteConfirm(
521+
Status.DeleteConfirm(
484522
statusKey = deeplinkRoute.statusKey,
485523
accountType = deeplinkRoute.accountType,
486524
)
525+
487526
is DeeplinkRoute.Status.Detail ->
488-
Route.Status.Detail(
527+
Status.Detail(
489528
statusKey = deeplinkRoute.statusKey,
490529
accountType = deeplinkRoute.accountType,
491530
)
531+
492532
is DeeplinkRoute.Status.MastodonReport ->
493-
Route.Status.MastodonReport(
533+
Status.MastodonReport(
494534
userKey = deeplinkRoute.userKey,
495535
statusKey = deeplinkRoute.statusKey,
496536
accountType = deeplinkRoute.accountType,
497537
)
538+
498539
is DeeplinkRoute.Status.MisskeyReport ->
499-
Route.Status.MisskeyReport(
540+
Status.MisskeyReport(
500541
userKey = deeplinkRoute.userKey,
501542
statusKey = deeplinkRoute.statusKey,
502543
accountType = deeplinkRoute.accountType,
503544
)
545+
504546
is DeeplinkRoute.Status.VVOComment ->
505-
Route.Status.VVOComment(
547+
Status.VVOComment(
506548
commentKey = deeplinkRoute.commentKey,
507549
accountType = deeplinkRoute.accountType,
508550
)
551+
509552
is DeeplinkRoute.Status.VVOStatus ->
510-
Route.Status.VVOStatus(
553+
Status.VVOStatus(
511554
statusKey = deeplinkRoute.statusKey,
512555
accountType = deeplinkRoute.accountType,
513556
)

0 commit comments

Comments
 (0)