mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
refactor(android): Remove AppLink in favor of nonce+state (#2826)
* Updates Android auth to remove AppLink authentication in favor of the custom URI -based scheme defined in #2823 * Default browser is opened instead of requiring Chrome Fixes #2703 --------- Signed-off-by: Jamil <jamilbk@users.noreply.github.com> Signed-off-by: Brian Manifold <bmanifold@users.noreply.github.com> Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Reactor Scram <ReactorScram@users.noreply.github.com> Co-authored-by: Andrew Dryga <andrew@dryga.com> Co-authored-by: Brian Manifold <bmanifold@users.noreply.github.com> Co-authored-by: Gabi <gabrielalejandro7@gmail.com> Co-authored-by: Thomas Eizinger <thomas@eizinger.io> Co-authored-by: Jason Elie Bou Kheir <5115126+jasonboukheir@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roopesh Chander <roop@roopc.net>
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
[
|
||||
{
|
||||
"relation": [
|
||||
"delegate_permission/common.handle_all_urls"
|
||||
],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "dev.firezone.android",
|
||||
"sha256_cert_fingerprints": [
|
||||
"82:86:46:E7:B7:FC:BD:01:4E:53:D5:92:E7:07:A6:90:B6:03:07:E5:02:E8:A9:20:EA:EE:54:6B:FA:E6:69:AA",
|
||||
"CB:FA:24:CE:CE:87:AF:5C:EF:C4:E4:E8:04:BC:C1:B8:34:0B:4E:7E:77:E1:26:80:41:6D:A8:44:56:DF:BA:A5",
|
||||
"70:FB:26:CB:DB:60:99:ED:E8:D1:11:7F:5F:0E:AC:35:9D:D9:23:14:24:9E:A0:35:C3:FC:5D:22:E2:08:EB:A8"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -46,44 +47,14 @@
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="dev.firezone.android.features.applink.ui.AppLinkHandlerActivity"
|
||||
android:name="dev.firezone.android.features.customuri.ui.CustomUriHandlerActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop">
|
||||
|
||||
<!-- Staging -->
|
||||
<intent-filter
|
||||
android:label="@string/app_name"
|
||||
android:autoVerify="true">
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- These must match the resulting URL from the Portal exactly.
|
||||
Don't use variables here otherwise this can break when testing in the emulator.
|
||||
For this to work in the emulator, you must use a host/IP that *both* the emulator and the
|
||||
host can access. E.g. 10.0.2.2 will not work. -->
|
||||
<data android:scheme="https" />
|
||||
<data android:host="app.firez.one" />
|
||||
<data android:pathPrefix="/handle_client_auth_callback" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Prod -->
|
||||
<intent-filter
|
||||
android:label="@string/app_name"
|
||||
android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<!-- These must match the resulting URL from the Portal exactly.
|
||||
Don't use variables here otherwise this can break when testing in the emulator.
|
||||
For this to work in the emulator, you must use a host/IP that *both* the emulator and the
|
||||
host can access. E.g. 10.0.2.2 will not work. -->
|
||||
<data android:scheme="https" />
|
||||
<data android:host="app.firezone.dev" />
|
||||
<data android:pathPrefix="/handle_client_auth_callback" />
|
||||
<data android:scheme="firezone-fd0020211111" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ package dev.firezone.android.core.data
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface AuthRepository {
|
||||
fun generateCsrfToken(): Flow<String>
|
||||
fun generateNonce(key: String): Flow<String>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.security.SecureRandom
|
||||
import java.util.Base64
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AuthRepositoryImpl
|
||||
@@ -16,23 +15,22 @@ internal class AuthRepositoryImpl
|
||||
private val coroutineDispatcher: CoroutineDispatcher,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) : AuthRepository {
|
||||
override fun generateCsrfToken(): Flow<String> =
|
||||
override fun generateNonce(key: String): Flow<String> =
|
||||
flow {
|
||||
val random = SecureRandom.getInstanceStrong()
|
||||
val bytes = ByteArray(CSRF_LENGTH)
|
||||
val bytes = ByteArray(NONCE_LENGTH)
|
||||
random.nextBytes(bytes)
|
||||
val encodedStr: String = Base64.getEncoder().encodeToString(bytes)
|
||||
val encodedStr: String = bytes.joinToString("") { "%02x".format(it) }
|
||||
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(CSRF_KEY, encodedStr)
|
||||
.putString(key, encodedStr)
|
||||
.apply()
|
||||
|
||||
emit(encodedStr)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
companion object {
|
||||
private const val CSRF_KEY = "csrf"
|
||||
private const val CSRF_LENGTH = 24
|
||||
private const val NONCE_LENGTH = 32
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ internal interface PreferenceRepository {
|
||||
|
||||
fun saveToken(value: String): Flow<Unit>
|
||||
|
||||
fun validateCsrfToken(value: String): Flow<Boolean>
|
||||
fun validateState(value: String): Flow<Boolean>
|
||||
|
||||
fun clearToken()
|
||||
|
||||
|
||||
@@ -48,24 +48,26 @@ internal class PreferenceRepositoryImpl
|
||||
|
||||
override fun saveToken(value: String): Flow<Unit> =
|
||||
flow {
|
||||
val nonce = sharedPreferences.getString(NONCE_KEY, "").orEmpty()
|
||||
emit(
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(TOKEN_KEY, value)
|
||||
.putString(TOKEN_KEY, nonce.plus(value))
|
||||
.apply(),
|
||||
)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun validateCsrfToken(value: String): Flow<Boolean> =
|
||||
override fun validateState(value: String): Flow<Boolean> =
|
||||
flow {
|
||||
val token = sharedPreferences.getString(CSRF_KEY, "").orEmpty()
|
||||
emit(MessageDigest.isEqual(token.toByteArray(), value.toByteArray()))
|
||||
val state = sharedPreferences.getString(STATE_KEY, "").orEmpty()
|
||||
emit(MessageDigest.isEqual(state.toByteArray(), value.toByteArray()))
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun clearToken() {
|
||||
sharedPreferences.edit().apply {
|
||||
remove(CSRF_KEY)
|
||||
remove(NONCE_KEY)
|
||||
remove(TOKEN_KEY)
|
||||
remove(STATE_KEY)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
@@ -79,6 +81,7 @@ internal class PreferenceRepositoryImpl
|
||||
private const val API_URL_KEY = "apiUrl"
|
||||
private const val LOG_FILTER_KEY = "logFilter"
|
||||
private const val TOKEN_KEY = "token"
|
||||
private const val CSRF_KEY = "csrf"
|
||||
private const val NONCE_KEY = "nonce"
|
||||
private const val STATE_KEY = "state"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import dev.firezone.android.core.data.AuthRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetCsrfTokenUseCase
|
||||
internal class GetNonceUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: AuthRepository,
|
||||
) {
|
||||
operator fun invoke(): Flow<String> = repository.generateCsrfToken()
|
||||
operator fun invoke(): Flow<String> = repository.generateNonce("nonce")
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.domain.auth
|
||||
|
||||
import dev.firezone.android.core.data.AuthRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetStateUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: AuthRepository,
|
||||
) {
|
||||
operator fun invoke(): Flow<String> = repository.generateNonce("state")
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import dev.firezone.android.core.data.PreferenceRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ValidateCsrfTokenUseCase
|
||||
internal class ValidateStateUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: PreferenceRepository,
|
||||
) {
|
||||
operator fun invoke(value: String): Flow<Boolean> = repository.validateCsrfToken(value)
|
||||
operator fun invoke(value: String): Flow<Boolean> = repository.validateState(value)
|
||||
}
|
||||
@@ -4,17 +4,13 @@ package dev.firezone.android.features.auth.ui
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.presentation.MainActivity
|
||||
import dev.firezone.android.databinding.ActivityAuthBinding
|
||||
import dev.firezone.android.util.CustomTabsHelper
|
||||
import java.lang.Exception
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AuthActivity : AppCompatActivity(R.layout.activity_auth) {
|
||||
@@ -48,30 +44,8 @@ class AuthActivity : AppCompatActivity(R.layout.activity_auth) {
|
||||
}
|
||||
|
||||
private fun setupWebView(url: String) {
|
||||
if (CustomTabsHelper.checkIfChromeIsInstalled(this)) {
|
||||
val intent = CustomTabsIntent.Builder().build()
|
||||
val packageName = CustomTabsHelper.getPackageNameToUse(this)
|
||||
if (CustomTabsHelper.checkIfChromeAppIsDefault()) {
|
||||
if (packageName != null) {
|
||||
intent.intent.setPackage(packageName)
|
||||
}
|
||||
} else {
|
||||
intent.intent.setPackage(CustomTabsHelper.STABLE_PACKAGE)
|
||||
}
|
||||
|
||||
try {
|
||||
intent.launchUrl(this@AuthActivity, Uri.parse(url))
|
||||
} catch (e: Exception) {
|
||||
showChromeAppRequiredError()
|
||||
}
|
||||
} else {
|
||||
showChromeAppRequiredError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showChromeAppRequiredError() {
|
||||
Toast.makeText(this, getString(R.string.signing_in_requires_chrome_browser), Toast.LENGTH_LONG).show()
|
||||
navigateToSignIn()
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun navigateToSignIn() {
|
||||
|
||||
@@ -6,7 +6,8 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.domain.auth.GetCsrfTokenUseCase
|
||||
import dev.firezone.android.core.domain.auth.GetNonceUseCase
|
||||
import dev.firezone.android.core.domain.auth.GetStateUseCase
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -18,7 +19,8 @@ internal class AuthViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getConfigUseCase: GetConfigUseCase,
|
||||
private val getCsrfTokenUseCase: GetCsrfTokenUseCase,
|
||||
private val getStateUseCase: GetStateUseCase,
|
||||
private val getNonceUseCase: GetNonceUseCase,
|
||||
) : ViewModel() {
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
@@ -32,9 +34,13 @@ internal class AuthViewModel
|
||||
getConfigUseCase()
|
||||
.firstOrNull() ?: throw Exception("config cannot be null")
|
||||
|
||||
val csrfToken =
|
||||
getCsrfTokenUseCase()
|
||||
.firstOrNull() ?: throw Exception("csrfToken cannot be null")
|
||||
val state =
|
||||
getStateUseCase()
|
||||
.firstOrNull() ?: throw Exception("state cannot be null")
|
||||
|
||||
val nonce =
|
||||
getNonceUseCase()
|
||||
.firstOrNull() ?: throw Exception("nonce cannot be null")
|
||||
|
||||
actionMutableLiveData.postValue(
|
||||
if (authFlowLaunched || config.token != null) {
|
||||
@@ -44,7 +50,7 @@ internal class AuthViewModel
|
||||
ViewAction.LaunchAuthFlow(
|
||||
url =
|
||||
"${config.authBaseUrl}" +
|
||||
"?client_csrf_token=$csrfToken&client_platform=android",
|
||||
"?state=$state&nonce=$nonce&as=client",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.features.applink.ui
|
||||
package dev.firezone.android.features.customuri.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
@@ -9,33 +9,33 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.presentation.MainActivity
|
||||
import dev.firezone.android.databinding.ActivityAppLinkHandlerBinding
|
||||
import dev.firezone.android.databinding.ActivityCustomUriHandlerBinding
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppLinkHandlerActivity : AppCompatActivity(R.layout.activity_app_link_handler) {
|
||||
private lateinit var binding: ActivityAppLinkHandlerBinding
|
||||
private val viewModel: AppLinkViewModel by viewModels()
|
||||
class CustomUriHandlerActivity : AppCompatActivity(R.layout.activity_custom_uri_handler) {
|
||||
private lateinit var binding: ActivityCustomUriHandlerBinding
|
||||
private val viewModel: CustomUriViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAppLinkHandlerBinding.inflate(layoutInflater)
|
||||
binding = ActivityCustomUriHandlerBinding.inflate(layoutInflater)
|
||||
|
||||
setupActionObservers()
|
||||
viewModel.parseAppLink(intent)
|
||||
viewModel.parseCustomUri(intent)
|
||||
}
|
||||
|
||||
private fun setupActionObservers() {
|
||||
viewModel.actionLiveData.observe(this) { action ->
|
||||
when (action) {
|
||||
AppLinkViewModel.ViewAction.AuthFlowComplete -> {
|
||||
CustomUriViewModel.ViewAction.AuthFlowComplete -> {
|
||||
startActivity(
|
||||
Intent(this@AppLinkHandlerActivity, MainActivity::class.java).apply {
|
||||
Intent(this@CustomUriHandlerActivity, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
},
|
||||
)
|
||||
finish()
|
||||
}
|
||||
AppLinkViewModel.ViewAction.ShowError -> showError()
|
||||
CustomUriViewModel.ViewAction.ShowError -> showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class AppLinkHandlerActivity : AppCompatActivity(R.layout.activity_app_link_hand
|
||||
.setPositiveButton(
|
||||
R.string.error_dialog_button_text,
|
||||
) { _, _ ->
|
||||
this@AppLinkHandlerActivity.finish()
|
||||
this@CustomUriHandlerActivity.finish()
|
||||
}
|
||||
.setIcon(R.drawable.ic_firezone_logo)
|
||||
.show()
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.features.applink.ui
|
||||
package dev.firezone.android.features.customuri.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
@@ -9,42 +9,43 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.domain.preference.SaveTokenUseCase
|
||||
import dev.firezone.android.core.domain.preference.ValidateCsrfTokenUseCase
|
||||
import dev.firezone.android.core.domain.preference.ValidateStateUseCase
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class AppLinkViewModel
|
||||
internal class CustomUriViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val validateCsrfTokenUseCase: ValidateCsrfTokenUseCase,
|
||||
private val validateStateUseCase: ValidateStateUseCase,
|
||||
private val saveTokenUseCase: SaveTokenUseCase,
|
||||
) : ViewModel() {
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
|
||||
fun parseAppLink(intent: Intent) {
|
||||
Log.d("AppLinkViewModel", "Parsing app link...")
|
||||
fun parseCustomUri(intent: Intent) {
|
||||
Log.d("CustomUriViewModel", "Parsing app link...")
|
||||
viewModelScope.launch {
|
||||
Log.d("AppLinkViewModel", "viewmodelScope.launch")
|
||||
Log.d("CustomUriViewModel", "viewmodelScope.launch")
|
||||
when (intent.data?.lastPathSegment) {
|
||||
PATH_CALLBACK -> {
|
||||
Log.d("AppLinkViewModel", "PATH_CALLBACK")
|
||||
intent.data?.getQueryParameter(QUERY_CLIENT_CSRF_TOKEN)?.let { csrfToken ->
|
||||
Log.d("AppLinkViewModel", "csrfToken: $csrfToken")
|
||||
if (validateCsrfTokenUseCase(csrfToken).firstOrNull() == true) {
|
||||
Log.d("AppLinkViewModel", "Valid CSRF token. Continuing to save token...")
|
||||
Log.d("CustomUriViewModel", "PATH_CALLBACK")
|
||||
intent.data?.getQueryParameter(QUERY_CLIENT_STATE)?.let { state ->
|
||||
Log.d("CustomUriViewModel", "state: $state")
|
||||
if (validateStateUseCase(state).firstOrNull() == true) {
|
||||
Log.d("CustomUriViewModel", "Valid state parameter. Continuing to save state...")
|
||||
} else {
|
||||
Log.d("AppLinkViewModel", "Invalid CSRF token! Continuing to save token anyway...")
|
||||
Log.d("CustomUriViewModel", "Invalid state parameter! Ignoring...")
|
||||
actionMutableLiveData.postValue(ViewAction.ShowError)
|
||||
}
|
||||
intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_TOKEN)?.let { token ->
|
||||
if (token.isNotBlank()) {
|
||||
Log.d("AppLinkViewModel", "Found valid auth token in response")
|
||||
saveTokenUseCase(token).collect()
|
||||
intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_FRAGMENT)?.let { fragment ->
|
||||
if (fragment.isNotBlank()) {
|
||||
Log.d("CustomUriViewModel", "Found valid auth fragment in response")
|
||||
saveTokenUseCase(fragment).collect()
|
||||
} else {
|
||||
Log.d("AppLinkViewModel", "Didn't find auth token in response!")
|
||||
Log.d("CustomUriViewModel", "Didn't find auth fragment in response!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +53,16 @@ internal class AppLinkViewModel
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d("AppLinkViewModel", "Unknown path segment: ${intent.data?.lastPathSegment}")
|
||||
Log.d("CustomUriViewModel", "Unknown path segment: ${intent.data?.lastPathSegment}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PATH_CALLBACK = "handle_client_auth_callback"
|
||||
private const val QUERY_CLIENT_CSRF_TOKEN = "client_csrf_token"
|
||||
private const val QUERY_CLIENT_AUTH_TOKEN = "client_auth_token"
|
||||
private const val PATH_CALLBACK = "handle_client_sign_in_callback"
|
||||
private const val QUERY_CLIENT_STATE = "state"
|
||||
private const val QUERY_CLIENT_AUTH_FRAGMENT = "fragment"
|
||||
private const val QUERY_ACTOR_NAME = "actor_name"
|
||||
private const val QUERY_IDENTITY_PROVIDER_IDENTIFIER = "identity_provider_identifier"
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2015 Firezone, Inc. */
|
||||
package dev.firezone.android.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
||||
class CustomTabsHelper {
|
||||
companion object {
|
||||
val STABLE_PACKAGE = "com.android.chrome"
|
||||
val BETA_PACKAGE = "com.chrome.beta"
|
||||
val DEV_PACKAGE = "com.chrome.dev"
|
||||
val LOCAL_PACKAGE = "com.google.android.apps.chrome"
|
||||
val ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService"
|
||||
|
||||
private var sPackageNameToUse: String? = null
|
||||
|
||||
fun getPackageNameToUse(context: Context): String? {
|
||||
if (sPackageNameToUse != null) return sPackageNameToUse
|
||||
val pm = context.getPackageManager()
|
||||
val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"))
|
||||
val defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0)
|
||||
var defaultViewHandlerPackageName: String? = null
|
||||
if (defaultViewHandlerInfo != null) {
|
||||
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName
|
||||
}
|
||||
val resolvedActivityList = pm.queryIntentActivities(activityIntent, 0)
|
||||
val packagesSupportingCustomTabs: MutableList<String> = ArrayList()
|
||||
for (info in resolvedActivityList) {
|
||||
val serviceIntent = Intent()
|
||||
serviceIntent.action = ACTION_CUSTOM_TABS_CONNECTION
|
||||
serviceIntent.setPackage(info.activityInfo.packageName)
|
||||
if (pm.resolveService(serviceIntent, 0) != null) {
|
||||
packagesSupportingCustomTabs.add(info.activityInfo.packageName)
|
||||
}
|
||||
}
|
||||
if (packagesSupportingCustomTabs.size == 1) {
|
||||
sPackageNameToUse = packagesSupportingCustomTabs.get(0)
|
||||
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
|
||||
sPackageNameToUse = STABLE_PACKAGE
|
||||
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
|
||||
sPackageNameToUse = BETA_PACKAGE
|
||||
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
|
||||
sPackageNameToUse = DEV_PACKAGE
|
||||
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
|
||||
sPackageNameToUse = LOCAL_PACKAGE
|
||||
}
|
||||
return sPackageNameToUse
|
||||
}
|
||||
|
||||
fun checkIfChromeAppIsDefault() =
|
||||
sPackageNameToUse == STABLE_PACKAGE ||
|
||||
sPackageNameToUse == BETA_PACKAGE ||
|
||||
sPackageNameToUse == DEV_PACKAGE ||
|
||||
sPackageNameToUse == LOCAL_PACKAGE
|
||||
|
||||
fun checkIfChromeIsInstalled(context: Context): Boolean =
|
||||
try {
|
||||
val info =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.packageManager.getPackageInfo(STABLE_PACKAGE, PackageManager.PackageInfoFlags.of(0L))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.packageManager.getPackageInfo(STABLE_PACKAGE, 0)
|
||||
}
|
||||
info.applicationInfo.enabled
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".features.applink.ui.AppLinkHandlerActivity">
|
||||
tools:context=".features.customuri.ui.CustomUriHandlerActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
Reference in New Issue
Block a user