diff options
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/MainActivity.kt')
-rw-r--r-- | wallet/src/main/java/net/taler/wallet/MainActivity.kt | 262 |
1 files changed, 215 insertions, 47 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt index 1dafce0..5dfd920 100644 --- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt +++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt @@ -32,8 +32,12 @@ import android.view.View.VISIBLE import android.widget.TextView import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf import androidx.core.view.GravityCompat.START +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration @@ -44,8 +48,15 @@ import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCal import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar -import com.google.zxing.integration.android.IntentIntegrator -import com.google.zxing.integration.android.IntentIntegrator.parseActivityResult +import com.google.zxing.client.android.Intents.Scan.MIXED_SCAN +import com.google.zxing.client.android.Intents.Scan.SCAN_TYPE +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions +import com.journeyapps.barcodescanner.ScanOptions.QR_CODE +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.taler.common.EventObserver import net.taler.common.isOnline import net.taler.common.showError import net.taler.wallet.BuildConfig.VERSION_CODE @@ -56,6 +67,9 @@ import net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_DISCONNEC import net.taler.wallet.HostCardEmulatorService.Companion.TRIGGER_PAYMENT_ACTION import net.taler.wallet.databinding.ActivityMainBinding import net.taler.wallet.refund.RefundStatus +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL import java.util.Locale.ROOT class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, @@ -66,6 +80,11 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, private lateinit var ui: ActivityMainBinding private lateinit var nav: NavController + private val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> + if (result == null || result.contents == null) return@registerForActivityResult + handleTalerUri(result.contents, "QR code") + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ui = ActivityMainBinding.inflate(layoutInflater) @@ -82,24 +101,23 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, setSupportActionBar(ui.content.toolbar) val appBarConfiguration = AppBarConfiguration( - setOf(R.id.nav_main, R.id.nav_settings, R.id.nav_pending_operations), + setOf(R.id.nav_main, R.id.nav_settings), ui.drawerLayout ) ui.content.toolbar.setupWithNavController(nav, appBarConfiguration) - model.showProgressBar.observe(this, { show -> + model.showProgressBar.observe(this) { show -> ui.content.progressBar.visibility = if (show) VISIBLE else INVISIBLE - }) + } val versionView: TextView = ui.navView.getHeaderView(0).findViewById(R.id.versionView) - model.devMode.observe(this, { enabled -> - ui.navView.menu.findItem(R.id.nav_dev).isVisible = enabled - if (enabled) { - @SuppressLint("SetTextI18n") - versionView.text = "$VERSION_NAME ($VERSION_CODE)" - versionView.visibility = VISIBLE - } else versionView.visibility = GONE - }) + @SuppressLint("SetTextI18n") + versionView.text = "$VERSION_NAME ($VERSION_CODE)" + + // Uncomment if any dev options are added in the future + // model.devMode.observe(this) { enabled -> + // ui.navView.menu.findItem(R.id.nav_dev).isVisible = enabled + // } if (intent.action == ACTION_VIEW) intent.dataString?.let { uri -> handleTalerUri(uri, "intent") @@ -111,32 +129,45 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, registerReceiver(nfcConnectedReceiver, IntentFilter(MERCHANT_NFC_CONNECTED)) registerReceiver(nfcDisconnectedReceiver, IntentFilter(MERCHANT_NFC_DISCONNECTED)) registerReceiver(tunnelResponseReceiver, IntentFilter(HTTP_TUNNEL_RESPONSE)) + + model.scanCodeEvent.observe(this, EventObserver { + val scanOptions = ScanOptions().apply { + setPrompt("") + setBeepEnabled(true) + setOrientationLocked(false) + setDesiredBarcodeFormats(QR_CODE) + addExtra(SCAN_TYPE, MIXED_SCAN) + } + if (it) barcodeLauncher.launch(scanOptions) + }) + + model.networkManager.networkStatus.observe(this) { online -> + ui.content.offlineBanner.visibility = if (online) GONE else VISIBLE + } } + @Deprecated("Deprecated in Java") override fun onBackPressed() { if (ui.drawerLayout.isDrawerOpen(START)) ui.drawerLayout.closeDrawer(START) else super.onBackPressed() } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (intent?.action == ACTION_VIEW) intent.dataString?.let { uri -> + handleTalerUri(uri, "intent") + } + } + override fun onNavigationItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.nav_home -> nav.navigate(R.id.nav_main) R.id.nav_settings -> nav.navigate(R.id.nav_settings) - R.id.nav_pending_operations -> nav.navigate(R.id.nav_pending_operations) } ui.drawerLayout.closeDrawer(START) return true } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == IntentIntegrator.REQUEST_CODE) { - parseActivityResult(requestCode, resultCode, data)?.contents?.let { contents -> - handleTalerUri(contents, "QR code") - } - } - } - override fun onDestroy() { unregisterReceiver(triggerPaymentReceiver) unregisterReceiver(nfcConnectedReceiver) @@ -145,29 +176,157 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, super.onDestroy() } + private fun getTalerAction( + uri: Uri, + maxRedirects: Int, + actionFound: MutableLiveData<String>, + ): MutableLiveData<String> { + val scheme = uri.scheme ?: return actionFound + + if (scheme == "http" || scheme == "https") { + model.viewModelScope.launch(Dispatchers.IO) { + val conn = URL(uri.toString()).openConnection() as HttpURLConnection + Log.v(TAG, "prepare query: $uri") + conn.setRequestProperty("Accept", "text/html") + conn.connectTimeout = 5000 + conn.requestMethod = "HEAD" + try { + conn.connect() + } catch (e: IOException) { + Log.e(TAG, "Error connecting to $uri ", e) + showError(R.string.error_broken_uri, "$uri") + return@launch + } + val status = conn.responseCode + + if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_PAYMENT_REQUIRED) { + val talerHeader = conn.headerFields["Taler"] + if (talerHeader != null && talerHeader[0] != null) { + Log.v(TAG, "taler header: ${talerHeader[0]}") + val talerHeaderUri = Uri.parse(talerHeader[0]) + getTalerAction(talerHeaderUri, 0, actionFound) + } + } else if (status == HttpURLConnection.HTTP_MOVED_TEMP + || status == HttpURLConnection.HTTP_MOVED_PERM + || status == HttpURLConnection.HTTP_SEE_OTHER + ) { + val location = conn.headerFields["Location"] + if (location != null && location[0] != null) { + Log.v(TAG, "location redirect: ${location[0]}") + val locUri = Uri.parse(location[0]) + getTalerAction(locUri, maxRedirects - 1, actionFound) + } + } else { + showError(R.string.error_broken_uri, "$uri") + } + } + } else { + actionFound.postValue(uri.toString()) + } + + return actionFound + } + private fun handleTalerUri(url: String, from: String) { val uri = Uri.parse(url) if (uri.fragment != null && !isOnline()) { connectToWifi(this, uri.fragment!!) } - when { - url.toLowerCase(ROOT).startsWith("taler://pay/") -> { - Log.v(TAG, "navigating!") - nav.navigate(R.id.action_nav_main_to_promptPayment) - model.paymentManager.preparePay(url) - } - url.toLowerCase(ROOT).startsWith("taler://withdraw/") -> { - Log.v(TAG, "navigating!") - // there's more than one entry point, so use global action - nav.navigate(R.id.action_global_promptWithdraw) - model.withdrawManager.getWithdrawalDetails(url) - } - url.toLowerCase(ROOT).startsWith("taler://refund/") -> { - model.showProgressBar.value = true - model.refundManager.refund(url).observe(this, Observer(::onRefundResponse)) + + getTalerAction(uri, 3, MutableLiveData<String>()).observe(this) { u -> + Log.v(TAG, "found action $u") + + if (u.startsWith("payto://", ignoreCase = true)) { + Log.v(TAG, "navigating with paytoUri!") + val bundle = bundleOf("uri" to u) + nav.navigate(R.id.action_nav_payto_uri, bundle) + return@observe } - else -> { - showError(R.string.error_unsupported_uri, "From: $from\nURI: $url") + + val normalizedURL = u.lowercase(ROOT) + var ext = false + val action = normalizedURL.substring( + if (normalizedURL.startsWith("taler://", ignoreCase = true)) { + "taler://".length + } else if (normalizedURL.startsWith("ext+taler://", ignoreCase = true)) { + ext = true + "ext+taler://".length + } else if (normalizedURL.startsWith("taler+http://", ignoreCase = true) && + model.devMode.value == true + ) { + "taler+http://".length + } else { + normalizedURL.length + } + ) + + // Remove ext+ scheme prefix if present + val u2 = if (ext) { + "taler://" + u.substring("ext+taler://".length) + } else u + + when { + action.startsWith("pay/", ignoreCase = true) -> { + Log.v(TAG, "navigating!") + nav.navigate(R.id.action_global_promptPayment) + model.paymentManager.preparePay(u2) + } + action.startsWith("withdraw/", ignoreCase = true) -> { + Log.v(TAG, "navigating!") + // there's more than one entry point, so use global action + nav.navigate(R.id.action_global_promptWithdraw) + model.withdrawManager.getWithdrawalDetails(u2) + } + + action.startsWith("withdraw-exchange/", ignoreCase = true) -> { + model.showProgressBar.value = true + lifecycleScope.launch(Dispatchers.IO) { + val response = model.withdrawManager.prepareManualWithdrawal(u2) + if (response == null) withContext(Dispatchers.Main) { + model.showProgressBar.value = false + nav.navigate(R.id.errorFragment) + } else { + val exchange = + model.exchangeManager.findExchangeByUrl(response.exchangeBaseUrl) + if (exchange == null) withContext(Dispatchers.Main) { + model.showProgressBar.value = false + showError(R.string.exchange_add_error) + } else { + model.exchangeManager.withdrawalExchange = exchange + withContext(Dispatchers.Main) { + model.showProgressBar.value = false + val args = Bundle().apply { + if (response.amount != null) { + putString("amount", response.amount.toJSONString()) + } + } + // there's more than one entry point, so use global action + nav.navigate(R.id.action_global_manual_withdrawal, args) + } + } + } + } + } + + action.startsWith("refund/", ignoreCase = true) -> { + model.showProgressBar.value = true + model.refundManager.refund(u2).observe(this, Observer(::onRefundResponse)) + } + action.startsWith("pay-pull/", ignoreCase = true) -> { + nav.navigate(R.id.action_global_prompt_pull_payment) + model.peerManager.preparePeerPullDebit(u2) + } + action.startsWith("pay-push/", ignoreCase = true) -> { + nav.navigate(R.id.action_global_prompt_push_payment) + model.peerManager.preparePeerPushCredit(u2) + } + action.startsWith("pay-template/", ignoreCase = true) -> { + val bundle = bundleOf("uri" to u2) + nav.navigate(R.id.action_global_prompt_pay_template, bundle) + } + else -> { + showError(R.string.error_unsupported_uri, "From: $from\nURI: $u2") + } } } } @@ -176,13 +335,23 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, model.showProgressBar.value = false when (status) { is RefundStatus.Error -> { - showError(R.string.refund_error, status.msg) + if (model.devMode.value == true) { + showError(status.error) + } else { + showError(R.string.refund_error, status.error.userFacingMsg) + } } is RefundStatus.Success -> { - val amount = status.response.amountRefundGranted - model.showTransactions(amount.currency) - val str = getString(R.string.refund_success, amount.amountStr) - Snackbar.make(ui.navView, str, LENGTH_LONG).show() + lifecycleScope.launch { + val transactionId = status.response.transactionId + val transaction = model.transactionManager.getTransactionById(transactionId) + if (transaction != null) { + // TODO: currency what? scopes are the cool thing now + // val currency = transaction.amountRaw.currency + // model.showTransactions(currency) + Snackbar.make(ui.navView, getString(R.string.refund_success), LENGTH_LONG).show() + } + } } } } @@ -222,10 +391,9 @@ class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener, override fun onPreferenceStartFragment( caller: PreferenceFragmentCompat, - pref: Preference + pref: Preference, ): Boolean { when (pref.key) { - "pref_backup" -> nav.navigate(R.id.action_nav_settings_to_nav_settings_backup) "pref_exchanges" -> nav.navigate(R.id.action_nav_settings_to_nav_settings_exchanges) } return true |