diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-08-20 22:55:02 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-08-20 22:55:02 +0200 |
commit | 884fa0b47579bef3cf29450acdce73c2ee5304fe (patch) | |
tree | 5da6fd067379ffa1947efd87d233957aa545bd7c /app/src/main/java | |
download | wallet-android-884fa0b47579bef3cf29450acdce73c2ee5304fe.tar.gz wallet-android-884fa0b47579bef3cf29450acdce73c2ee5304fe.tar.bz2 wallet-android-884fa0b47579bef3cf29450acdce73c2ee5304fe.zip |
import into repo
Diffstat (limited to 'app/src/main/java')
-rw-r--r-- | app/src/main/java/net/taler/wallet/MainActivity.kt | 138 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/PaymentSuccessful.kt | 37 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/PromptPayment.kt | 143 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/Settings.kt | 48 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/ShowBalance.kt | 164 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/WalletHistory.kt | 31 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/WalletViewModel.kt | 331 |
7 files changed, 892 insertions, 0 deletions
diff --git a/app/src/main/java/net/taler/wallet/MainActivity.kt b/app/src/main/java/net/taler/wallet/MainActivity.kt new file mode 100644 index 0000000..e539cfd --- /dev/null +++ b/app/src/main/java/net/taler/wallet/MainActivity.kt @@ -0,0 +1,138 @@ +package net.taler.wallet + + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.setupWithNavController +import com.google.android.material.navigation.NavigationView +import com.google.android.material.snackbar.Snackbar +import com.google.zxing.integration.android.IntentIntegrator +import com.google.zxing.integration.android.IntentResult +import me.zhanghai.android.materialprogressbar.MaterialProgressBar + +class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { + + lateinit var model: WalletViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + val toolbar: Toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + + val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout) + val navView: NavigationView = findViewById(R.id.nav_view) + + navView.menu.getItem(0).isChecked = true + + + navView.setNavigationItemSelectedListener(this) + + val navController = findNavController(R.id.nav_host_fragment) + val appBarConfiguration = + AppBarConfiguration(setOf(R.id.showBalance, R.id.settings, R.id.walletHistory), drawerLayout) + + findViewById<Toolbar>(R.id.toolbar) + .setupWithNavController(navController, appBarConfiguration) + + model = ViewModelProviders.of(this)[WalletViewModel::class.java] + + val progressBar = findViewById<MaterialProgressBar>(R.id.progress_bar) + progressBar.visibility = View.INVISIBLE + + model.init() + model.getBalances() + } + + override fun onBackPressed() { + val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout) + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START) + } else { + super.onBackPressed() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + return when (item.itemId) { + R.id.action_settings -> true + else -> super.onOptionsItemSelected(item) + } + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean { + // Handle navigation view item clicks here. + when (item.itemId) { + R.id.nav_home -> { + findNavController(R.id.nav_host_fragment).navigate(R.id.showBalance) + } + R.id.nav_settings -> { + findNavController(R.id.nav_host_fragment).navigate(R.id.settings) + } + R.id.nav_history -> { + findNavController(R.id.nav_host_fragment).navigate(R.id.walletHistory) + } + } + val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout) + drawerLayout.closeDrawer(GravityCompat.START) + return true + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode != IntentIntegrator.REQUEST_CODE) { + return + } + + val scanResult: IntentResult? = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + + if (scanResult == null || scanResult.contents == null) { + val bar: Snackbar = Snackbar.make( + findViewById(R.id.nav_host_fragment), + "QR Code scan canceled.", + Snackbar.LENGTH_SHORT + ) + bar.show() + return + } + + val url = scanResult.contents!! + if (!url.startsWith("talerpay:")) { + val bar: Snackbar = Snackbar.make( + findViewById(R.id.nav_host_fragment), + "Scanned QR code doesn't contain Taler payment.", + Snackbar.LENGTH_SHORT + ) + bar.show() + return + } + + Log.v(TAG, "navigating!") + + findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptPayment) + model.preparePay(url) + } + + +} diff --git a/app/src/main/java/net/taler/wallet/PaymentSuccessful.kt b/app/src/main/java/net/taler/wallet/PaymentSuccessful.kt new file mode 100644 index 0000000..039fa73 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/PaymentSuccessful.kt @@ -0,0 +1,37 @@ +package net.taler.wallet + + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.navigation.findNavController + + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * + */ +class PaymentSuccessful : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val view = inflater.inflate(R.layout.fragment_payment_successful, container, false) + view.findViewById<Button>(R.id.button_success_back).setOnClickListener { + activity!!.findNavController(R.id.nav_host_fragment).navigateUp() + } + return view + } + + +} diff --git a/app/src/main/java/net/taler/wallet/PromptPayment.kt b/app/src/main/java/net/taler/wallet/PromptPayment.kt new file mode 100644 index 0000000..9486814 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/PromptPayment.kt @@ -0,0 +1,143 @@ +package net.taler.wallet + + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.navigation.findNavController +import com.google.android.material.snackbar.Snackbar +import me.zhanghai.android.materialprogressbar.MaterialProgressBar + + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * + */ +class PromptPayment : Fragment() { + + lateinit var model: WalletViewModel + + var fragmentView: View? = null + + fun triggerLoading(loading: Boolean) { + val myActivity = activity!! + val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar) + if (loading) { + progressBar.visibility = View.VISIBLE + } else { + progressBar.visibility = View.INVISIBLE + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + model = activity?.run { + ViewModelProviders.of(this)[WalletViewModel::class.java] + } ?: throw Exception("Invalid Activity") + + triggerLoading(true) + } + + override fun onResume() { + super.onResume() + Log.v("taler-wallet", "called onResume on PromptPayment") + triggerLoading(model.payStatus.value == null || model.payStatus.value is PayStatus.Loading) + } + + fun fillOrderInfo(view: View, contractTerms: ContractTerms, totalFees: Amount?) { + val feesAmountView = view.findViewById<TextView>(R.id.order_fees_amount) + val amountView = view.findViewById<TextView>(R.id.order_amount) + val summaryView = view.findViewById<TextView>(R.id.order_summary) + summaryView.text = contractTerms.summary + val amount = contractTerms.amount + @SuppressLint("SetTextI18n") + amountView.text = "${amount.amount} ${amount.currency}" + val feesBox = view.findViewById<View>(R.id.order_fees_box) + if (totalFees != null) { + @SuppressLint("SetTextI18n") + feesAmountView.text = "${totalFees.amount} ${totalFees.currency}" + feesBox.visibility = View.VISIBLE + } else { + feesBox.visibility = View.INVISIBLE + } + + } + + fun showPayStatus(view: View, payStatus: PayStatus) { + val promptPaymentDetails = view.findViewById<View>(R.id.prompt_payment_details) + val balanceInsufficientWarning = view.findViewById<View>(R.id.balance_insufficient_warning) + val confirmPaymentButton = view.findViewById<Button>(R.id.button_confirm_payment) + when (payStatus) { + is PayStatus.Prepared -> { + fillOrderInfo(view, payStatus.contractTerms, payStatus.totalFees) + promptPaymentDetails.visibility = View.VISIBLE + balanceInsufficientWarning.visibility = View.GONE + confirmPaymentButton.isEnabled = true + + confirmPaymentButton.setOnClickListener { + model.confirmPay(payStatus.proposalId) + triggerLoading(true) + confirmPaymentButton.isEnabled = false + } + triggerLoading(false) + } + is PayStatus.InsufficientBalance -> { + fillOrderInfo(view, payStatus.contractTerms, null) + promptPaymentDetails.visibility = View.VISIBLE + balanceInsufficientWarning.visibility = View.VISIBLE + confirmPaymentButton.isEnabled = false + triggerLoading(false) + } + is PayStatus.Success -> { + triggerLoading(false) + activity!!.findNavController(R.id.nav_host_fragment).navigate(R.id.action_promptPayment_to_paymentSuccessful) + } + else -> { + val bar = Snackbar.make(view , "Unexpected result", Snackbar.LENGTH_SHORT) + bar.show() + } + } + } + + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + + val view = inflater.inflate(R.layout.fragment_prompt_payment, container, false) + fragmentView = view + + val promptPaymentDetails = view.findViewById<View>(R.id.prompt_payment_details) + // Set invisible until data is loaded. + promptPaymentDetails.visibility = View.INVISIBLE + + val abortPaymentButton = view.findViewById<Button>(R.id.button_abort_payment) + + abortPaymentButton.setOnClickListener { + activity!!.findNavController(R.id.nav_host_fragment).navigateUp() + } + + triggerLoading(true) + + model.payStatus.observe(this, Observer { + showPayStatus(view, it) + }) + return view + } +} diff --git a/app/src/main/java/net/taler/wallet/Settings.kt b/app/src/main/java/net/taler/wallet/Settings.kt new file mode 100644 index 0000000..bf67f56 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/Settings.kt @@ -0,0 +1,48 @@ +package net.taler.wallet + + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.lifecycle.ViewModelProviders + + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * + */ +class Settings : Fragment() { + + private lateinit var model: WalletViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + model = activity?.run { + ViewModelProviders.of(this)[WalletViewModel::class.java] + } ?: throw Exception("Invalid Activity") + + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val view = inflater.inflate(R.layout.fragment_settings, container, false) + view.findViewById<Button>(R.id.button_reset_wallet_dangerously).setOnClickListener { + model.dangerouslyReset() + } + return view + } + + +} diff --git a/app/src/main/java/net/taler/wallet/ShowBalance.kt b/app/src/main/java/net/taler/wallet/ShowBalance.kt new file mode 100644 index 0000000..ba1bf14 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/ShowBalance.kt @@ -0,0 +1,164 @@ +package net.taler.wallet + + +import android.app.Activity +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar +import com.google.zxing.integration.android.IntentIntegrator +import me.zhanghai.android.materialprogressbar.MaterialProgressBar + +class MyAdapter(private var myDataset: WalletBalances) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() { + + init { + setHasStableIds(false) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { + val rowView = LayoutInflater.from(parent.context).inflate(R.layout.balance_row, parent, false) + return MyViewHolder(rowView) + } + + override fun getItemCount(): Int { + return myDataset.byCurrency.size + } + + override fun onBindViewHolder(holder: MyViewHolder, position: Int) { + val currencyView = holder.rowView.findViewById<TextView>(R.id.balance_currency) + currencyView.text = myDataset.byCurrency[position].currency + val amountView = holder.rowView.findViewById<TextView>(R.id.balance_amount) + amountView.text = myDataset.byCurrency[position].amount + } + + fun update(updatedBalances: WalletBalances) { + this.myDataset = updatedBalances + this.notifyDataSetChanged() + } + + class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView) +} + + +/** + * A simple [Fragment] subclass. + * + */ +class ShowBalance : Fragment() { + + lateinit var balancesView: RecyclerView + lateinit var balancesPlaceholderView: TextView + lateinit var model: WalletViewModel + lateinit var balancesAdapter: MyAdapter + + fun triggerLoading() { + val loading: Boolean = + (model.isBalanceLoading.value == true) || (model.balances.value == null) || !model.balances.value!!.initialized + + val myActivity = activity!! + val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar) + if (loading) { + progressBar.visibility = View.VISIBLE + } else { + progressBar.visibility = View.INVISIBLE + } + } + + override fun onResume() { + super.onResume() + triggerLoading() + Log.v("taler-wallet", "called onResume on ShowBalance") + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + model = activity?.run { + ViewModelProviders.of(this)[WalletViewModel::class.java] + } ?: throw Exception("Invalid Activity") + + + model.isBalanceLoading.observe(this, Observer { loading -> + Log.v("taler-wallet", "observing balance loading ${loading} in show balance") + triggerLoading() + }) + } + + + private fun updateBalances(balances: WalletBalances) { + if (!balances.initialized) { + balancesPlaceholderView.visibility = View.GONE + balancesView.visibility = View.GONE + } else if (balances.byCurrency.isEmpty()) { + balancesPlaceholderView.visibility = View.VISIBLE + balancesView.visibility = View.GONE + } else { + balancesPlaceholderView.visibility = View.GONE + balancesView.visibility = View.VISIBLE + } + Log.v(TAG, "updating balances ${balances}") + balancesAdapter.update(balances) + //this.balancesView.adapter = balancesAdapter + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val view = inflater.inflate(R.layout.fragment_show_balance, container, false) + val payQrButton = view.findViewById<Button>(R.id.button_pay_qr) + payQrButton.setOnClickListener { + val integrator = IntentIntegrator(activity) + integrator.setPrompt("Place merchant's QR Code inside the viewfinder rectangle to initiate payment.") + integrator.initiateScan(listOf("QR_CODE")) + } + + val withdrawTestkudosButton = view.findViewById<Button>(R.id.button_withdraw_testkudos) + withdrawTestkudosButton.setOnClickListener { + model.withdrawTestkudos() + } + + val payNfcButton = view.findViewById<Button>(R.id.button_pay_nfc) + payNfcButton.setOnClickListener { + val bar: Snackbar = Snackbar.make(view, "Sorry, NFC is not implemented yet!", Snackbar.LENGTH_SHORT) + bar.show() + } + + + this.balancesView = view.findViewById(R.id.list_balances) + this.balancesPlaceholderView = view.findViewById(R.id.list_balances_placeholder) + + val myLayoutManager = LinearLayoutManager(context) + val myItemDecoration = DividerItemDecoration(context, myLayoutManager.orientation) + + val balances = model.balances.value!! + + balancesAdapter = MyAdapter(balances) + + view.findViewById<RecyclerView>(R.id.list_balances).apply { + layoutManager = myLayoutManager + adapter = balancesAdapter + addItemDecoration(myItemDecoration) + } + + updateBalances(balances) + + model.balances.observe(this, Observer { + triggerLoading() + updateBalances(it) + }) + + return view + } +} diff --git a/app/src/main/java/net/taler/wallet/WalletHistory.kt b/app/src/main/java/net/taler/wallet/WalletHistory.kt new file mode 100644 index 0000000..baf825e --- /dev/null +++ b/app/src/main/java/net/taler/wallet/WalletHistory.kt @@ -0,0 +1,31 @@ +package net.taler.wallet + + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * + */ +class WalletHistory : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_show_history, container, false) + } + + +} diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt new file mode 100644 index 0000000..7c82f81 --- /dev/null +++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt @@ -0,0 +1,331 @@ +package net.taler.wallet + +import akono.AkonoJni +import akono.ModuleResult +import android.app.Application +import android.content.res.AssetManager +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import org.json.JSONObject +import java.io.InputStream +import java.lang.Exception +import java.nio.file.Paths + +val TAG = "taler-wallet" + +class AssetModuleLoader(private val assetManager: AssetManager, private val rootPath: String = "node_modules") : + AkonoJni.LoadModuleHandler { + + private fun makeResult(localPath: String, stream: InputStream): ModuleResult { + val moduleString = stream.bufferedReader().use { + it.readText() + } + return ModuleResult("/vmodroot/$localPath", moduleString) + } + + private fun tryPath(rawAssetPath: String): ModuleResult? { + val assetPath = Paths.get(rawAssetPath).normalize().toString() + try { + val moduleStream = assetManager.open(assetPath) + return makeResult(assetPath, moduleStream) + } catch (e: Exception) { + } + try { + val jsPath = "$assetPath.js" + val moduleStream = assetManager.open(jsPath) + return makeResult(jsPath, moduleStream) + } catch (e: Exception) { + // ignore + } + val packageJsonPath = "$assetPath/package.json" + try { + val packageStream = assetManager.open(packageJsonPath) + val packageString = packageStream.bufferedReader().use { + it.readText() + } + val packageJson = JSONObject(packageString) + val mainFile = try { + packageJson.getString("main") + } catch (e: Exception) { + Log.w(TAG, "package.json does not have a 'main' filed") + throw e + } + Log.i(TAG, "main field is $mainFile") + try { + val modPath = Paths.get("$assetPath/$mainFile").normalize().toString() + return makeResult(modPath, assetManager.open(modPath)) + } catch (e: Exception) { + // ignore + } + try { + val modPath = Paths.get("$assetPath/$mainFile.js").normalize().toString() + return makeResult(modPath, assetManager.open(modPath)) + } catch (e: Exception) { + } + } catch (e: Exception) { + } + try { + val jsPath = "$assetPath/index.js" + Log.i(TAG, "trying to open $jsPath") + val moduleStream = assetManager.open(jsPath) + return makeResult(jsPath, moduleStream) + } catch (e: Exception) { + } + return null + } + + override fun loadModule(name: String, paths: Array<String>): ModuleResult? { + Log.i(TAG, "loading module $name from paths [${paths.fold("", { acc, s -> "$acc,$s" })}]") + for (path in paths) { + Log.i(TAG, "trying from path $path") + val prefix = "/vmodroot" + if (!path.startsWith(prefix)) { + continue + } + if (path == prefix) { + Log.i(TAG, "path is prefix") + val res = tryPath("$rootPath/$name") + if (res != null) + return res + } else { + Log.i(TAG, "path is not prefix") + val res = tryPath(path.drop(prefix.length + 1) + "/$name") + if (res != null) + return res + } + } + return null + } +} + + +class AssetDataHandler(private val assetManager: AssetManager) : AkonoJni.GetDataHandler { + override fun handleGetData(what: String): ByteArray? { + if (what == "taler-emscripten-lib.wasm") { + Log.i(TAG, "loading emscripten binary from taler-wallet") + val stream = assetManager.open("node_modules/taler-wallet/emscripten/taler-emscripten-lib.wasm") + val bytes: ByteArray = stream.readBytes() + Log.i(TAG, "size of emscripten binary: ${bytes.size}") + return bytes + } else { + Log.w(TAG, "data '$what' requested by akono not found") + return null + } + } +} + +data class Amount(val currency: String, val amount: String) { + companion object { + val FRACTIONAL_BASE = 1e8; + fun fromJson(jsonAmount: JSONObject): Amount { + val amountCurrency = jsonAmount.getString("currency") + val amountValue = jsonAmount.getString("value") + val amountFraction = jsonAmount.getString("fraction") + val amountIntValue = Integer.parseInt(amountValue) + val amountIntFraction = Integer.parseInt(amountFraction) + return Amount(amountCurrency, (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString()) + } + fun fromString(strAmount: String): Amount { + val components = strAmount.split(":") + return Amount(components[0], components[1]) + } + } +} + + +data class WalletBalances(val initialized: Boolean, val byCurrency: List<Amount>) + +data class ContractTerms(val summary: String, val amount: Amount) + +open class PayStatus { + class Loading : PayStatus() + data class Prepared(val contractTerms: ContractTerms, val proposalId: Int, val totalFees: Amount) : PayStatus() + data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus() + data class AlreadyPaid(val contractTerms: ContractTerms) : PayStatus() + data class Error(val error: String) : PayStatus() + class Success : PayStatus() +} + + +class WalletViewModel(val app: Application) : AndroidViewModel(app) { + private lateinit var myAkono: AkonoJni + private var initialized = false + + private var withdrawInProgress: Int = 0 + + val balances: MutableLiveData<WalletBalances> = MutableLiveData() + + val isBalanceLoading: MutableLiveData<Boolean> = MutableLiveData() + + //val isProposalLoading: MutableLiveData<Boolean> = MutableLiveData() + + val payStatus: MutableLiveData<PayStatus> = MutableLiveData() + + init { + isBalanceLoading.value = false + balances.value = WalletBalances(false, listOf()) + } + + fun init() { + if (initialized) { + Log.e(TAG, "WalletViewModel already initialized") + return + } + val app = this.getApplication<Application>() + myAkono = AkonoJni() + myAkono.setLoadModuleHandler(AssetModuleLoader(app.assets)) + myAkono.setGetDataHandler(AssetDataHandler(app.assets)) + myAkono.setMessageHandler(object : AkonoJni.MessageHandler { + override fun handleMessage(messageStr: String) { + Log.v(TAG, "got back message: ${messageStr}") + val message = JSONObject(messageStr) + val type = message.getString("type") + when (type) { + "notification" -> { + getBalances() + } + "response" -> { + val operation = message.getString("operation") + Log.v(TAG, "got response for operation $operation") + when (operation) { + "withdrawTestkudos" -> { + withdrawInProgress-- + if (withdrawInProgress == 0) { + isBalanceLoading.postValue(false) + } + } + "getBalances" -> { + val balanceList = mutableListOf<Amount>(); + val result = message.getJSONObject("result") + val byCurrency = result.getJSONObject("byCurrency") + val currencyList = byCurrency.keys().asSequence().toList().sorted() + for (currency in currencyList) { + val jsonAmount = byCurrency.getJSONObject(currency).getJSONObject("available") + val amount = Amount.fromJson(jsonAmount) + balanceList.add(amount) + } + balances.postValue(WalletBalances(true, balanceList)) + } + "preparePay" -> { + Log.v(TAG, "got preparePay result") + val result = message.getJSONObject("result") + val status = result.getString("status") + var contractTerms: ContractTerms? = null + var proposalId: Int? = null + var totalFees: Amount? = null + if (result.has("proposalId")) { + proposalId = result.getInt("proposalId") + } + if (result.has("contractTerms")) { + val ctJson = result.getJSONObject("contractTerms") + val amount = Amount.fromString(ctJson.getString("amount")) + val summary = ctJson.getString("summary") + contractTerms = ContractTerms(summary, amount) + } + if (result.has("totalFees")) { + totalFees = Amount.fromJson(result.getJSONObject("totalFees")) + } + val res = when (status) { + "payment-possible" -> PayStatus.Prepared(contractTerms!!, proposalId!!, totalFees!!) + "paid" -> PayStatus.AlreadyPaid(contractTerms!!) + "insufficient-balance" -> PayStatus.InsufficientBalance(contractTerms!!) + "error" -> PayStatus.Error("got some error") + else -> PayStatus.Error("unkown status") + } + payStatus.postValue(res) + } + "confirmPay" -> { + payStatus.postValue(PayStatus.Success()) + } + } + + } + } + } + }) + + myAkono.evalNodeCode("console.log('hello world from taler wallet-android')") + myAkono.evalNodeCode("tw = require('taler-wallet');") + myAkono.evalNodeCode("tw.installAndroidWalletListener();") + + sendInitMessage() + + this.initialized = true + } + + private fun sendInitMessage() { + val msg = JSONObject() + msg.put("operation", "init") + val args = JSONObject() + msg.put("args", args) + args.put("persistentStoragePath", "${app.filesDir}/talerwalletdb.json") + + Log.v(TAG, "sending message ${msg}") + + myAkono.sendMessage(msg.toString()) + } + + fun getBalances() { + if (!initialized) { + Log.e(TAG, "WalletViewModel not initialized") + return + } + + val msg = JSONObject() + msg.put("operation", "getBalances") + + myAkono.sendMessage(msg.toString()) + } + + fun withdrawTestkudos() { + if (!initialized) { + Log.e(TAG, "WalletViewModel not initialized") + return + } + + withdrawInProgress++ + this.isBalanceLoading.value = true + + val msg = JSONObject() + msg.put("operation", "withdrawTestkudos") + + myAkono.sendMessage(msg.toString()) + } + + fun preparePay(url: String) { + val msg = JSONObject() + msg.put("operation", "preparePay") + + val args = JSONObject() + msg.put("args", args) + args.put("url", url) + + myAkono.sendMessage(msg.toString()) + } + + fun confirmPay(proposalId: Int) { + val msg = JSONObject() + msg.put("operation", "confirmPay") + + val args = JSONObject() + msg.put("args", args) + args.put("proposalId", proposalId) + + myAkono.sendMessage(msg.toString()) + } + + fun dangerouslyReset() { + val msg = JSONObject() + msg.put("operation", "reset") + + myAkono.sendMessage(msg.toString()) + + sendInitMessage() + + isBalanceLoading.value = false + balances.value = WalletBalances(false, listOf()) + + getBalances() + } +}
\ No newline at end of file |