Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+38
View File
@@ -0,0 +1,38 @@
apply plugin: 'kotlin-parcelize'
android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
namespace 'io.novafoundation.nova.feature_deep_linking'
buildFeatures {
viewBinding true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':common')
implementation kotlinDep
implementation androidDep
implementation materialDep
implementation branchIo
implementation playServiceIdentifier
implementation coroutinesDep
implementation daggerDep
ksp daggerCompiler
androidTestImplementation androidTestRunnerDep
androidTestImplementation androidTestRulesDep
androidTestImplementation androidJunitDep
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
@@ -0,0 +1,16 @@
package io.novafoundation.nova.feature_deep_linking.di
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
import io.novafoundation.nova.feature_deep_linking.presentation.handling.PendingDeepLinkProvider
import io.novafoundation.nova.feature_deep_linking.presentation.handling.branchIo.BranchIoLinkConverter
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkingPreferences
interface DeepLinkingFeatureApi {
val deepLinkingPreferences: DeepLinkingPreferences
val pendingDeepLinkProvider: PendingDeepLinkProvider
val branchIoLinkConverter: BranchIoLinkConverter
val linkBuilderFactory: LinkBuilderFactory
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_deep_linking.di
import dagger.Component
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.scope.FeatureScope
@Component(
dependencies = [
DeepLinkingFeatureDependencies::class
],
modules = [
DeepLinkingFeatureModule::class
]
)
@FeatureScope
interface DeepLinkingFeatureComponent : DeepLinkingFeatureApi {
@Component.Factory
interface Factory {
fun create(
deps: DeepLinkingFeatureDependencies
): DeepLinkingFeatureComponent
}
@Component(
dependencies = [
CommonApi::class
]
)
interface DeepLinkingFeatureDependenciesComponent : DeepLinkingFeatureDependencies
}
@@ -0,0 +1,35 @@
package io.novafoundation.nova.feature_deep_linking.di
import android.content.Context
import coil.ImageLoader
import com.google.gson.Gson
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
import io.novafoundation.nova.common.resources.ContextManager
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.coroutines.RootScope
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
interface DeepLinkingFeatureDependencies {
val rootScope: RootScope
val preferences: Preferences
val context: Context
val contextManager: ContextManager
val permissionsAskerFactory: PermissionsAskerFactory
val resourceManager: ResourceManager
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
val imageLoader: ImageLoader
val gson: Gson
val automaticInteractionGate: AutomaticInteractionGate
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_deep_linking.di
import io.novafoundation.nova.common.di.FeatureApiHolder
import io.novafoundation.nova.common.di.FeatureContainer
import javax.inject.Inject
class DeepLinkingFeatureHolder @Inject constructor(
featureContainer: FeatureContainer
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val dependencies = DaggerDeepLinkingFeatureComponent_DeepLinkingFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.build()
return DaggerDeepLinkingFeatureComponent.factory()
.create(
deps = dependencies
)
}
}
@@ -0,0 +1,46 @@
package io.novafoundation.nova.feature_deep_linking.di
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_deep_linking.R
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
import io.novafoundation.nova.feature_deep_linking.presentation.handling.PendingDeepLinkProvider
import io.novafoundation.nova.feature_deep_linking.presentation.handling.branchIo.BranchIoLinkConverter
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkingPreferences
@Module
class DeepLinkingFeatureModule {
@Provides
@FeatureScope
fun provideDeepLinkPreferences(
resourceManager: ResourceManager
) = DeepLinkingPreferences(
deepLinkScheme = resourceManager.getString(R.string.deep_linking_scheme),
deepLinkHost = resourceManager.getString(R.string.deep_linking_host),
appLinkHost = resourceManager.getString(R.string.app_link_host),
branchIoLinkHosts = listOf(
resourceManager.getString(R.string.branch_io_link_host),
resourceManager.getString(R.string.branch_io_link_host_alternate)
)
)
@Provides
@FeatureScope
fun provideLinkBuilderFactory(preferences: DeepLinkingPreferences) = LinkBuilderFactory(preferences)
@Provides
@FeatureScope
fun providePendingDeepLinkProvider(preferences: Preferences): PendingDeepLinkProvider {
return PendingDeepLinkProvider(preferences)
}
@Provides
@FeatureScope
fun provideBranchIoLinkConverter(
deepLinkingPreferences: DeepLinkingPreferences
) = BranchIoLinkConverter(deepLinkingPreferences)
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_deep_linking.presentation.configuring
import android.net.Uri
interface DeepLinkConfigurator<T> {
enum class Type {
APP_LINK, DEEP_LINK
}
fun configure(payload: T, type: Type = Type.DEEP_LINK): Uri
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_deep_linking.presentation.configuring
import android.content.Intent
fun <T> Intent.applyDeepLink(configurator: DeepLinkConfigurator<T>, payload: T): Intent {
data = configurator.configure(payload)
return this
}
@@ -0,0 +1,110 @@
package io.novafoundation.nova.feature_deep_linking.presentation.configuring
import android.net.Uri
import io.novafoundation.nova.common.utils.appendPathOrSkip
import io.novafoundation.nova.feature_deep_linking.presentation.handling.branchIo.BranchIOConstants
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkingPreferences
interface LinkBuilder {
fun setAction(action: String): LinkBuilder
fun setEntity(entity: String): LinkBuilder
fun setScreen(screen: String): LinkBuilder
fun addParam(key: String, value: String): LinkBuilder
fun build(): Uri
}
class LinkBuilderFactory(private val deepLinkingPreferences: DeepLinkingPreferences) {
fun newLink(type: DeepLinkConfigurator.Type): LinkBuilder {
return when (type) {
DeepLinkConfigurator.Type.APP_LINK -> AppLinkBuilderType(deepLinkingPreferences)
DeepLinkConfigurator.Type.DEEP_LINK -> DeepLinkBuilderType(deepLinkingPreferences)
}
}
}
class DeepLinkBuilderType(
deepLinkingPreferences: DeepLinkingPreferences
) : LinkBuilder {
private var action: String? = null
private var entity: String? = null
private var screen: String? = null
private val urlBuilder = Uri.Builder()
.scheme(deepLinkingPreferences.deepLinkScheme)
.authority(deepLinkingPreferences.deepLinkHost)
override fun setAction(action: String): LinkBuilder {
this.action = action
return this
}
override fun setEntity(entity: String): LinkBuilder {
this.entity = entity
return this
}
override fun setScreen(screen: String): LinkBuilder {
this.screen = screen
return this
}
override fun addParam(key: String, value: String): LinkBuilder {
urlBuilder.appendQueryParameter(key, value)
return this
}
override fun build(): Uri {
val finalPath = Uri.Builder()
.appendPathOrSkip(action)
.appendPathOrSkip(entity)
.appendPathOrSkip(screen)
.build()
.path
return urlBuilder.path(finalPath).build()
}
}
class AppLinkBuilderType(
private val deepLinkingPreferences: DeepLinkingPreferences
) : LinkBuilder {
private val urlBuilder = Uri.Builder()
.scheme("https")
.authority(deepLinkingPreferences.branchIoLinkHosts.first())
override fun setAction(action: String): LinkBuilder {
urlBuilder.appendQueryParameter(BranchIOConstants.ACTION_QUERY, action)
return this
}
override fun setEntity(entity: String): LinkBuilder {
urlBuilder.appendQueryParameter(BranchIOConstants.ENTITY_QUERY, entity)
return this
}
override fun setScreen(screen: String): LinkBuilder {
urlBuilder.appendQueryParameter(BranchIOConstants.SCREEN_QUERY, screen)
return this
}
override fun addParam(key: String, value: String): LinkBuilder {
urlBuilder.appendQueryParameter(key, value)
return this
}
override fun build(): Uri {
return urlBuilder.build()
}
}
fun LinkBuilder.addParamIfNotNull(name: String, value: String?) = apply {
value?.let { addParam(name, value) }
}
@@ -0,0 +1,17 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling
import android.net.Uri
import kotlinx.coroutines.flow.Flow
interface DeepLinkHandler {
val callbackFlow: Flow<CallbackEvent>
suspend fun matches(data: Uri): Boolean
suspend fun handleDeepLink(data: Uri): Result<Unit>
}
sealed interface CallbackEvent {
data class Message(val message: String) : CallbackEvent
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling
import android.net.Uri
import io.novafoundation.nova.common.data.storage.Preferences
private const val PREFS_PENDING_DEEP_LINK = "pending_deep_link"
class PendingDeepLinkProvider(
private val preferences: Preferences
) {
fun save(data: Uri) {
preferences.putString(PREFS_PENDING_DEEP_LINK, data.toString())
}
fun get(): Uri? {
val deepLink = preferences.getString(PREFS_PENDING_DEEP_LINK) ?: return null
return Uri.parse(deepLink)
}
fun clear() {
preferences.removeField(PREFS_PENDING_DEEP_LINK)
}
}
@@ -0,0 +1,45 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling
import android.net.Uri
import io.novafoundation.nova.common.utils.onFailureInstance
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge
class RootDeepLinkHandler(
private val pendingDeepLinkProvider: PendingDeepLinkProvider,
private val nestedHandlers: Collection<DeepLinkHandler>
) : DeepLinkHandler {
override val callbackFlow: Flow<CallbackEvent> = nestedHandlers
.mapNotNull { it.callbackFlow }
.merge()
override suspend fun matches(data: Uri): Boolean {
return nestedHandlers.any { it.matches(data) }
}
suspend fun checkAndHandlePendingDeepLink(): Result<Unit> {
val pendingDeepLink = pendingDeepLinkProvider.get() ?: return Result.failure(IllegalStateException("No pending deep link found"))
return handleDeepLinkInternal(pendingDeepLink)
.onSuccess { pendingDeepLinkProvider.clear() }
}
override suspend fun handleDeepLink(data: Uri): Result<Unit> {
pendingDeepLinkProvider.save(data)
return handleDeepLinkInternal(data)
.onSuccess { pendingDeepLinkProvider.clear() }
.onFailureInstance<HandlerNotFoundException, Unit> { pendingDeepLinkProvider.clear() } // If we haven't find any handler - no need to save deep link
}
private suspend fun handleDeepLinkInternal(data: Uri): Result<Unit> {
val firstHandler = nestedHandlers.find { it.canHandle(data) } ?: return Result.failure(HandlerNotFoundException())
return firstHandler.handleDeepLink(data)
}
private suspend fun DeepLinkHandler.canHandle(data: Uri) = runCatching { this.matches(data) }
.getOrDefault(false)
}
private class HandlerNotFoundException : Exception()
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling.branchIo
object BranchIOConstants {
const val ACTION_QUERY = "action"
const val SCREEN_QUERY = "screen"
const val ENTITY_QUERY = "entity"
}
@@ -0,0 +1,56 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling.branchIo
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import io.branch.referral.Branch
import io.branch.referral.Defines
import io.novafoundation.nova.common.utils.LOG_TAG
import io.novafoundation.nova.feature_deep_linking.BuildConfig
class BranchIOLinkHandler(
private val deepLinkFactory: BranchIoLinkConverter
) {
object Initializer {
fun init(context: Context) {
if (BuildConfig.DEBUG) {
Branch.enableLogging()
}
val branchInstance = Branch.getAutoInstance(context)
branchInstance.setConsumerProtectionAttributionLevel(Defines.BranchAttributionLevel.REDUCED)
}
}
fun onActivityStart(activity: Activity, deepLinkCallback: (Uri) -> Unit) {
Branch.sessionBuilder(activity)
.withCallback { branchUniversalObject, _, error ->
if (error != null) {
Log.e(LOG_TAG, error.toString())
}
if (branchUniversalObject != null) {
val deepLink = deepLinkFactory.formatToDeepLink(branchUniversalObject)
deepLinkCallback(deepLink)
}
}
.withData(activity.intent.data)
.init()
}
fun onActivityNewIntent(activity: Activity, intent: Intent?) {
if (intent != null && intent.getBooleanExtra("branch_force_new_session", false)) {
Branch.sessionBuilder(activity)
.withCallback { _, error ->
if (error != null) {
Log.e(LOG_TAG, error.toString())
}
}
.withData(intent.data)
.reInit()
}
}
}
@@ -0,0 +1,44 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling.branchIo
import android.net.Uri
import io.branch.indexing.BranchUniversalObject
import io.novafoundation.nova.common.utils.appendPathOrSkip
import io.novafoundation.nova.common.utils.appendQueries
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkingPreferences
private val BRANCH_PARAMS_PREFIX = listOf("~", "$", "+")
class BranchIoLinkConverter(
private val deepLinkingPreferences: DeepLinkingPreferences
) {
fun formatToDeepLink(data: BranchUniversalObject): Uri {
val queries = data.contentMetadata.customMetadata
.excludeInternalIOQueries()
.toMutableMap()
return Uri.Builder()
.scheme(deepLinkingPreferences.deepLinkScheme)
.authority(deepLinkingPreferences.deepLinkHost)
.appendPathOrSkip(queries.extractAction())
.appendPathOrSkip(queries.extractSubject())
.appendQueries(queries)
.build()
}
private fun Map<String, String>.excludeInternalIOQueries(): Map<String, String> {
return filterKeys { key ->
val isBranchIOQuery = BRANCH_PARAMS_PREFIX.any { prefix -> key.startsWith(prefix) }
!isBranchIOQuery
}
}
private fun MutableMap<String, String>.extractAction(): String? {
return remove(BranchIOConstants.ACTION_QUERY)
}
private fun MutableMap<String, String>.extractSubject(): String? {
return remove(BranchIOConstants.SCREEN_QUERY)
?: remove(BranchIOConstants.ENTITY_QUERY)
}
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling.common
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkHandlingException.DAppHandlingException
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkHandlingException.ImportMnemonicHandlingException
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkHandlingException.ReferendumHandlingException
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_deep_linking.R
fun formatDeepLinkHandlingException(resourceManager: ResourceManager, exception: DeepLinkHandlingException): String {
return when (exception) {
is ReferendumHandlingException -> handleReferendumException(resourceManager, exception)
is DAppHandlingException -> handleDAppException(resourceManager, exception)
is ImportMnemonicHandlingException -> handleImportMnemonicException(resourceManager, exception)
}
}
private fun handleReferendumException(resourceManager: ResourceManager, exception: ReferendumHandlingException): String {
return when (exception) {
ReferendumHandlingException.ReferendumIsNotSpecified -> resourceManager.getString(R.string.referendim_details_not_found_title)
ReferendumHandlingException.ChainIsNotFound -> resourceManager.getString(R.string.deep_linking_chain_id_is_not_found)
ReferendumHandlingException.GovernanceTypeIsNotSpecified -> resourceManager.getString(R.string.deep_linking_governance_type_is_not_specified)
ReferendumHandlingException.GovernanceTypeIsNotSupported -> resourceManager.getString(R.string.deep_linking_governance_type_is_not_supported)
}
}
fun handleDAppException(resourceManager: ResourceManager, exception: DAppHandlingException): String {
return when (exception) {
DAppHandlingException.UrlIsInvalid -> resourceManager.getString(R.string.deep_linking_url_is_invalid)
is DAppHandlingException.DomainIsNotMatched -> resourceManager.getString(R.string.deep_linking_domain_is_not_matched, exception.domain)
}
}
fun handleImportMnemonicException(resourceManager: ResourceManager, exception: ImportMnemonicHandlingException): String {
return when (exception) {
ImportMnemonicHandlingException.InvalidMnemonic -> resourceManager.getString(R.string.deep_linking_invalid_mnemonic)
ImportMnemonicHandlingException.InvalidCryptoType -> resourceManager.getString(R.string.deep_linking_invalid_crypto_type)
ImportMnemonicHandlingException.InvalidDerivationPath -> resourceManager.getString(R.string.deep_linking_invalid_derivation_path)
}
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling.common
sealed class DeepLinkHandlingException : Exception() {
sealed class ReferendumHandlingException : DeepLinkHandlingException() {
object ReferendumIsNotSpecified : ReferendumHandlingException()
object ChainIsNotFound : ReferendumHandlingException()
object GovernanceTypeIsNotSpecified : ReferendumHandlingException()
object GovernanceTypeIsNotSupported : ReferendumHandlingException()
}
sealed class DAppHandlingException : DeepLinkHandlingException() {
object UrlIsInvalid : DAppHandlingException()
class DomainIsNotMatched(val domain: String) : DAppHandlingException()
}
sealed class ImportMnemonicHandlingException : DeepLinkHandlingException() {
object InvalidMnemonic : ImportMnemonicHandlingException()
object InvalidCryptoType : ImportMnemonicHandlingException()
object InvalidDerivationPath : ImportMnemonicHandlingException()
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_deep_linking.presentation.handling.common
import android.net.Uri
class DeepLinkingPreferences(
val deepLinkScheme: String,
val deepLinkHost: String,
val appLinkHost: String,
val branchIoLinkHosts: List<String>
)
fun Uri.isDeepLink(preferences: DeepLinkingPreferences): Boolean {
return scheme == preferences.deepLinkScheme && host == preferences.deepLinkHost
}