diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts index 316aa1dde..70ef1872c 100644 --- a/app-android/build.gradle.kts +++ b/app-android/build.gradle.kts @@ -91,5 +91,6 @@ dependencies { implementation(libs.composeNavigation) implementation(libs.composeHiltNavigtation) implementation(libs.accompanistSystemUiController) + implementation(libs.androidxBrowser) testImplementation(projects.core.testing) } \ No newline at end of file diff --git a/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt b/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt index 1dfb931c1..fc88aafe3 100644 --- a/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt +++ b/app-android/src/main/java/io/github/droidkaigi/confsched2023/KaigiApp.kt @@ -1,5 +1,13 @@ package io.github.droidkaigi.confsched2023 +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -7,8 +15,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder @@ -78,9 +89,10 @@ fun KaigiApp(modifier: Modifier = Modifier) { @Composable private fun KaigiNavHost( navController: NavHostController = rememberNavController(), + externalNavController: ExternalNavController = rememberExternalNavController(), ) { NavHost(navController = navController, startDestination = mainScreenRoute) { - mainScreen(navController) + mainScreen(navController, externalNavController) sessionScreens( onNavigationIconClick = { navController.popBackStack() @@ -104,7 +116,10 @@ private fun KaigiNavHost( } } -private fun NavGraphBuilder.mainScreen(navController: NavHostController) { +private fun NavGraphBuilder.mainScreen( + navController: NavHostController, + externalNavController: ExternalNavController, +) { mainScreen( mainNestedGraphStateHolder = KaigiAppMainNestedGraphStateHolder(), mainNestedGraph = { mainNestedNavController, padding -> @@ -129,11 +144,11 @@ private fun NavGraphBuilder.mainScreen(navController: NavHostController) { CodeOfConduct -> TODO() Contributors -> TODO() License -> TODO() - Medium -> TODO() + Medium -> externalNavController.navigate(url = "https://medium.com/droidkaigi") PrivacyPolicy -> TODO() Staff -> TODO() - X -> TODO() - YouTube -> TODO() + X -> externalNavController.navigate(url = "https://twitter.com/DroidKaigi") + YouTube -> externalNavController.navigate(url = "https://www.youtube.com/c/DroidKaigi") } }, ) @@ -181,3 +196,84 @@ class KaigiAppMainNestedGraphStateHolder : MainNestedGraphStateHolder { } } } + +@Composable +private fun rememberExternalNavController(): ExternalNavController { + val context = LocalContext.current + return remember(context) { + ExternalNavController(context = context) + } +} + +private class ExternalNavController( + private val context: Context, +) { + + fun navigate(url: String) { + val uri: Uri = url.toUri() + val launched = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + navigateToNativeAppApi30(context = context, uri = uri) + } else { + navigateToNativeApp(context = context, uri = uri) + } + if (launched.not()) { + navigateToCustomTab(context = context, uri = uri) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun navigateToNativeAppApi30(context: Context, uri: Uri): Boolean { + val nativeAppIntent = Intent(Intent.ACTION_VIEW, uri) + .addCategory(Intent.CATEGORY_BROWSABLE) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER) + return try { + context.startActivity(nativeAppIntent) + true + } catch (ex: ActivityNotFoundException) { + false + } + } + + @SuppressLint("QueryPermissionsNeeded") + private fun navigateToNativeApp(context: Context, uri: Uri): Boolean { + val pm = context.packageManager + + // Get all Apps that resolve a generic url + val browserActivityIntent = Intent() + .setAction(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(Uri.fromParts("http", "", null)) + val genericResolvedList: Set = + pm.queryIntentActivities(browserActivityIntent, 0) + .map { it.activityInfo.packageName } + .toSet() + + // Get all apps that resolve the specific Url + val specializedActivityIntent = Intent(Intent.ACTION_VIEW, uri) + .addCategory(Intent.CATEGORY_BROWSABLE) + val resolvedSpecializedList: MutableSet = + pm.queryIntentActivities(browserActivityIntent, 0) + .map { it.activityInfo.packageName } + .toMutableSet() + + // Keep only the Urls that resolve the specific, but not the generic urls. + resolvedSpecializedList.removeAll(genericResolvedList) + + // If the list is empty, no native app handlers were found. + if (resolvedSpecializedList.isEmpty()) { + return false + } + + // We found native handlers. Launch the Intent. + specializedActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(specializedActivityIntent) + return true + } + + private fun navigateToCustomTab(context: Context, uri: Uri) { + CustomTabsIntent.Builder() + .setShowTitle(true) + .build() + .launchUrl(context, uri) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f8594abf6..ccc0bdde8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -83,6 +83,7 @@ androidxAppCompat = { module = "androidx.appcompat:appcompat", version = "1.7.0- androidxLifecycleLifecycleRuntimeKtx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } androidxActivityActivityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } androidxDatastoreDatastorePreferences = { module = "androidx.datastore:datastore-preferences-core", version = "1.1.0-alpha04" } +androidxBrowser = { module = "androidx.browser:browser", version = "1.6.0" } javaPoet = { module = "com.squareup:javapoet", version = "1.13.0" } # Data