summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-03 14:42:32 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-03 14:42:32 +0100
commit80cd3c9d6d83798316a1222f3875d3a0e74ca278 (patch)
tree77bb114b1c47199d41d788c1e968c692eb07a727
parent87c1b63c4cf2b81963735feb0bce8b8f0b004dba (diff)
downloadwallet-android-80cd3c9d6d83798316a1222f3875d3a0e74ca278.tar.gz
wallet-android-80cd3c9d6d83798316a1222f3875d3a0e74ca278.tar.bz2
wallet-android-80cd3c9d6d83798316a1222f3875d3a0e74ca278.zip
UI tweaks
-rw-r--r--.idea/gradle.xml1
-rw-r--r--app/build.gradle2
-rw-r--r--app/src/main/java/net/taler/wallet/MainActivity.kt4
-rw-r--r--app/src/main/java/net/taler/wallet/ShowBalance.kt95
-rw-r--r--app/src/main/java/net/taler/wallet/WalletHistory.kt101
-rw-r--r--app/src/main/java/net/taler/wallet/WalletViewModel.kt70
-rw-r--r--app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt4
-rw-r--r--app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt65
-rw-r--r--app/src/main/java/net/taler/wallet/crypto/Encoding.kt116
-rw-r--r--app/src/main/res/layout/fragment_settings.xml4
-rw-r--r--app/src/main/res/layout/fragment_show_balance.xml13
-rw-r--r--app/src/main/res/layout/fragment_show_history.xml32
-rw-r--r--app/src/main/res/layout/history_row.xml15
-rw-r--r--app/src/main/res/layout/pending_row.xml15
-rw-r--r--app/src/main/res/menu/balance.xml12
-rw-r--r--app/src/main/res/menu/history.xml8
-rw-r--r--app/src/main/res/navigation/nav_graph.xml2
-rw-r--r--app/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt20
18 files changed, 507 insertions, 72 deletions
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index afc8041..031c262 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -13,6 +13,7 @@
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
+ <option name="testRunner" value="PLATFORM" />
</GradleProjectSettings>
</option>
</component>
diff --git a/app/build.gradle b/app/build.gradle
index e49c3d8..8270882 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,7 +5,7 @@ apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
- buildToolsVersion "29.0.1"
+ buildToolsVersion "29.0.2"
defaultConfig {
applicationId "net.taler.wallet"
minSdkVersion 18
diff --git a/app/src/main/java/net/taler/wallet/MainActivity.kt b/app/src/main/java/net/taler/wallet/MainActivity.kt
index 3f19986..fa0b1d9 100644
--- a/app/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/app/src/main/java/net/taler/wallet/MainActivity.kt
@@ -127,8 +127,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
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
+ //menuInflater.inflate(R.menu.main, menu)
+ return false
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
diff --git a/app/src/main/java/net/taler/wallet/ShowBalance.kt b/app/src/main/java/net/taler/wallet/ShowBalance.kt
index a45d4fa..ce07816 100644
--- a/app/src/main/java/net/taler/wallet/ShowBalance.kt
+++ b/app/src/main/java/net/taler/wallet/ShowBalance.kt
@@ -3,10 +3,8 @@ package net.taler.wallet
import android.os.Bundle
import android.util.Log
+import android.view.*
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
@@ -17,7 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.zxing.integration.android.IntentIntegrator
import me.zhanghai.android.materialprogressbar.MaterialProgressBar
-class MyAdapter(private var myDataset: WalletBalances) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
+class WalletBalanceAdapter(private var myDataset: WalletBalances) : RecyclerView.Adapter<WalletBalanceAdapter.MyViewHolder>() {
init {
setHasStableIds(false)
@@ -59,6 +57,35 @@ class MyAdapter(private var myDataset: WalletBalances) : RecyclerView.Adapter<My
class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
}
+class PendingOperationsAdapter(private var myDataset: PendingOperations) : RecyclerView.Adapter<PendingOperationsAdapter.MyViewHolder>() {
+
+ init {
+ setHasStableIds(false)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
+ val rowView = LayoutInflater.from(parent.context).inflate(R.layout.pending_row, parent, false)
+ return MyViewHolder(rowView)
+ }
+
+ override fun getItemCount(): Int {
+ return myDataset.pending.size
+ }
+
+ override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
+ val p = myDataset.pending[position]
+ val textView = holder.rowView.findViewById<TextView>(R.id.pending_text)
+ textView.text = p.type
+ }
+
+ fun update(updatedDataset: PendingOperations) {
+ this.myDataset = updatedDataset
+ this.notifyDataSetChanged()
+ }
+
+ class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
+}
+
/**
* A simple [Fragment] subclass.
@@ -66,12 +93,15 @@ class MyAdapter(private var myDataset: WalletBalances) : RecyclerView.Adapter<My
*/
class ShowBalance : Fragment() {
+ private lateinit var pendingOperationsLabel: View
lateinit var balancesView: RecyclerView
lateinit var balancesPlaceholderView: TextView
lateinit var model: WalletViewModel
- lateinit var balancesAdapter: MyAdapter
+ lateinit var balancesAdapter: WalletBalanceAdapter
+
+ lateinit var pendingAdapter: PendingOperationsAdapter
- fun triggerLoading() {
+ private fun triggerLoading() {
val loading: Boolean =
(model.testWithdrawalInProgress.value == true) || (model.balances.value == null) || !model.balances.value!!.initialized
@@ -90,8 +120,29 @@ class ShowBalance : Fragment() {
Log.v("taler-wallet", "called onResume on ShowBalance")
}
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.retry_pending -> {
+ model.retryPendingNow()
+ true
+ }
+ R.id.reload_balance -> {
+ triggerLoading()
+ model.balances.value = WalletBalances(false, listOf())
+ model.getBalances()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ activity?.menuInflater?.inflate(R.menu.balance, menu)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
model = activity?.run {
ViewModelProviders.of(this)[WalletViewModel::class.java]
@@ -113,7 +164,15 @@ class ShowBalance : Fragment() {
}
Log.v(TAG, "updating balances ${balances}")
balancesAdapter.update(balances)
- //this.balancesView.adapter = balancesAdapter
+ }
+
+ private fun updatePending(pendingOperations: PendingOperations) {
+ if (pendingOperations.pending.size == 0) {
+ pendingOperationsLabel.visibility = View.GONE
+ } else {
+ pendingOperationsLabel.visibility = View.VISIBLE
+ }
+ pendingAdapter.update(pendingOperations)
}
override fun onCreateView(
@@ -137,14 +196,14 @@ class ShowBalance : Fragment() {
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)
+ balancesAdapter = WalletBalanceAdapter(balances)
view.findViewById<RecyclerView>(R.id.list_balances).apply {
+ val myLayoutManager = LinearLayoutManager(context)
+ val myItemDecoration = DividerItemDecoration(context, myLayoutManager.orientation)
layoutManager = myLayoutManager
adapter = balancesAdapter
addItemDecoration(myItemDecoration)
@@ -163,6 +222,22 @@ class ShowBalance : Fragment() {
triggerLoading()
})
+ pendingAdapter = PendingOperationsAdapter(PendingOperations(listOf()))
+
+ this.pendingOperationsLabel = view.findViewById<View>(R.id.pending_operations_label)
+
+ view.findViewById<RecyclerView>(R.id.list_pending).apply {
+ val myLayoutManager = LinearLayoutManager(context)
+ val myItemDecoration = DividerItemDecoration(context, myLayoutManager.orientation)
+ layoutManager = myLayoutManager
+ adapter = pendingAdapter
+ addItemDecoration(myItemDecoration)
+ }
+
+ model.pendingOperations.observe(this, Observer {
+ updatePending(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
index baf825e..6c22ad1 100644
--- a/app/src/main/java/net/taler/wallet/WalletHistory.kt
+++ b/app/src/main/java/net/taler/wallet/WalletHistory.kt
@@ -2,30 +2,111 @@ package net.taler.wallet
import android.os.Bundle
+import android.view.*
import androidx.fragment.app.Fragment
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
+import android.widget.TextView
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+class WalletHistoryAdapter(private var myDataset: HistoryResult) : RecyclerView.Adapter<WalletHistoryAdapter.MyViewHolder>() {
+
+ init {
+ setHasStableIds(false)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
+ val rowView = LayoutInflater.from(parent.context).inflate(R.layout.history_row, parent, false)
+ return MyViewHolder(rowView)
+ }
+
+ override fun getItemCount(): Int {
+ return myDataset.history.size
+ }
+
+ override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
+ val text = holder.rowView.findViewById<TextView>(R.id.history_text)
+ text.text = myDataset.history[position].type
+ }
+
+ fun update(updatedHistory: HistoryResult) {
+ this.myDataset = updatedHistory
+ this.notifyDataSetChanged()
+ }
+
+ class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
+}
-// 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.
+ * Wallet history.
*
*/
class WalletHistory : Fragment() {
+ lateinit var model: WalletViewModel
+ lateinit var historyAdapter: WalletHistoryAdapter
+ lateinit var historyPlaceholder: View
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+
+ historyAdapter = WalletHistoryAdapter(HistoryResult(listOf()))
+
+ model = activity?.run {
+ ViewModelProviders.of(this)[WalletViewModel::class.java]
+ } ?: throw Exception("Invalid Activity")
+
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ activity?.menuInflater?.inflate(R.menu.history, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.reload_history -> {
+ updateHistory()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun updateHistory() {
+ model.getHistory {
+ if (it.history.size == 0) {
+ historyPlaceholder.visibility = View.VISIBLE
+ }
+ historyAdapter.update(it)
+ }
+ }
+
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)
- }
+ val view = inflater.inflate(R.layout.fragment_show_history, container, false)
+ val myLayoutManager = LinearLayoutManager(context)
+ val myItemDecoration = DividerItemDecoration(context, myLayoutManager.orientation)
+ view.findViewById<RecyclerView>(R.id.list_history).apply {
+ layoutManager = myLayoutManager
+ adapter = historyAdapter
+ addItemDecoration(myItemDecoration)
+ }
+
+ historyPlaceholder = view.findViewById<View>(R.id.list_history_placeholder)
+ historyPlaceholder.visibility = View.GONE
+ updateHistory()
+ return view
+ }
+
+ companion object {
+ const val TAG = "taler-wallet"
+ }
}
diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
index 34e8eed..dffc4a0 100644
--- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt
+++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
@@ -48,7 +48,7 @@ open class PayStatus {
class Loading : PayStatus()
data class Prepared(
val contractTerms: ContractTerms,
- val proposalId: Int,
+ val proposalId: String,
val totalFees: Amount
) : PayStatus()
@@ -71,6 +71,24 @@ open class WithdrawStatus {
data class Withdrawing(val talerWithdrawUri: String) : WithdrawStatus()
}
+open class HistoryResult(
+ val history: List<HistoryEntry>
+)
+
+open class HistoryEntry(
+ val detail: JSONObject,
+ val type: String
+)
+
+open class PendingOperationInfo(
+ val type: String,
+ val detail: JSONObject
+)
+
+open class PendingOperations(
+ val pending: List<PendingOperationInfo>
+)
+
class WalletViewModel(val app: Application) : AndroidViewModel(app) {
private var initialized = false
@@ -91,6 +109,10 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
value = WithdrawStatus.None()
}
+ val pendingOperations = MutableLiveData<PendingOperations>().apply {
+ value = PendingOperations(listOf())
+ }
+
private val walletBackendApi = WalletBackendApi(app)
fun init() {
@@ -101,9 +123,13 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
this.initialized = true
+ getBalances()
+ getPending()
+
walletBackendApi.notificationHandler = {
Log.i(TAG, "got notification from wallet")
getBalances()
+ getPending()
}
}
@@ -126,6 +152,37 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
}
}
+ private fun getPending() {
+ walletBackendApi.sendRequest("getPendingOperations", null) { result ->
+ Log.i(TAG, "got getPending result")
+ val pendingList = mutableListOf<PendingOperationInfo>()
+ val pendingJson = result.getJSONArray("pendingOperations")
+ for (i in 0 until pendingJson.length()) {
+ val p = pendingJson.getJSONObject(i)
+ val type = p.getString("type")
+ pendingList.add(PendingOperationInfo(type, p))
+ }
+ Log.i(TAG, "Got ${pendingList.size} pending operations")
+ pendingOperations.postValue(PendingOperations((pendingList)))
+ }
+ }
+
+ fun getHistory(cb: (r: HistoryResult) -> Unit) {
+ walletBackendApi.sendRequest("getHistory", null) { result ->
+ val historyEntries = mutableListOf<HistoryEntry>()
+ val historyList = result.getJSONArray("history")
+ for (i in 0 until historyList.length()) {
+ val h = historyList.getJSONObject(i)
+ Log.v(TAG, "got history entry $h")
+ val type = h.getString("type")
+ Log.v(TAG, "got history entry type $type")
+ val detail = h.getJSONObject("detail")
+ historyEntries.add(HistoryEntry(detail, type))
+ }
+ cb(HistoryResult(historyEntries))
+ }
+ }
+
fun withdrawTestkudos() {
testWithdrawalInProgress.value = true
@@ -144,10 +201,10 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
Log.v(TAG, "got preparePay result")
val status = result.getString("status")
var contractTerms: ContractTerms? = null
- var proposalId: Int? = null
+ var proposalId: String? = null
var totalFees: Amount? = null
if (result.has("proposalId")) {
- proposalId = result.getInt("proposalId")
+ proposalId = result.getString("proposalId")
}
if (result.has("contractTerms")) {
val ctJson = result.getJSONObject("contractTerms")
@@ -175,7 +232,7 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
}
}
- fun confirmPay(proposalId: Int) {
+ fun confirmPay(proposalId: String) {
val msg = JSONObject()
msg.put("operation", "confirmPay")
@@ -191,7 +248,6 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
walletBackendApi.sendRequest("reset", null)
testWithdrawalInProgress.value = false
balances.value = WalletBalances(false, listOf())
- getBalances()
}
fun startTunnel() {
@@ -249,6 +305,10 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
}
}
+ fun retryPendingNow() {
+ walletBackendApi.sendRequest("retryPendingNow", null)
+ }
+
override fun onCleared() {
walletBackendApi.destroy()
super.onCleared()
diff --git a/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
index 45c719d..31e0f08 100644
--- a/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -43,6 +43,8 @@ class WalletBackendApi(private val app: Application) {
when (msg.what) {
WalletBackendService.MSG_REPLY -> {
val requestID = msg.data.getInt("requestID", 0)
+ val operation = msg.data.getString("operation", "??")
+ Log.i(TAG, "got reply for operation $operation ($requestID)")
val h = api.handlers.get(requestID)
if (h == null) {
Log.e(TAG, "request ID not associated with a handler")
@@ -87,8 +89,8 @@ class WalletBackendApi(private val app: Application) {
args: JSONObject?,
onResponse: (message: JSONObject) -> Unit = { }
) {
- Log.i(TAG, "sending request for operation $operation")
val requestID = nextRequestID++
+ Log.i(TAG, "sending request for operation $operation ($requestID)")
val msg = Message.obtain(null, WalletBackendService.MSG_COMMAND)
handlers.put(requestID, onResponse)
msg.replyTo = incomingMessenger
diff --git a/app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt b/app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
index 916dfdb..b72ce6b 100644
--- a/app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
+++ b/app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
@@ -14,11 +14,13 @@ import net.taler.wallet.HostCardEmulatorService
import org.json.JSONObject
import java.io.File
import java.io.InputStream
+import java.lang.Process
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.ConcurrentHashMap
+import kotlin.system.exitProcess
-val TAG = "taler-wallet-backend"
+private const val TAG = "taler-wallet-backend"
/**
* Module loader to handle module loading requests from the wallet-core running on node/v8.
@@ -110,17 +112,7 @@ private class AssetModuleLoader(
private 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
- }
+ return null
}
}
@@ -144,6 +136,7 @@ class WalletBackendService : Service() {
private val subscribers = LinkedList<Messenger>()
override fun onCreate() {
+ Log.i(TAG, "onCreate in wallet backend service")
akono = AkonoJni()
akono.setLoadModuleHandler(AssetModuleLoader(application.assets))
akono.setGetDataHandler(AssetDataHandler(application.assets))
@@ -161,13 +154,12 @@ class WalletBackendService : Service() {
super.onCreate()
}
- fun sendInitMessage() {
+ private fun sendInitMessage() {
val msg = JSONObject()
msg.put("operation", "init")
val args = JSONObject()
msg.put("args", args)
args.put("persistentStoragePath", "${application.filesDir}/talerwalletdb.json")
-
akono.sendMessage(msg.toString())
}
@@ -242,30 +234,34 @@ class WalletBackendService : Service() {
return messenger.binder
}
+ private fun sendNotify() {
+ var rm: LinkedList<Messenger>? = null
+ for (s in subscribers) {
+ val m = Message.obtain(null, MSG_NOTIFY)
+ try {
+ s.send(m)
+ } catch (e: RemoteException) {
+ if (rm == null) {
+ rm = LinkedList<Messenger>()
+ }
+ rm.add(s)
+ subscribers.remove(s)
+ }
+ }
+ if (rm != null) {
+ for (s in rm) {
+ subscribers.remove(s)
+ }
+ }
+ }
+
private fun handleAkonoMessage(messageStr: String) {
Log.v(TAG, "got back message: ${messageStr}")
val message = JSONObject(messageStr)
val type = message.getString("type")
when (type) {
"notification" -> {
- var rm: LinkedList<Messenger>? = null
- for (s in subscribers) {
- val m = Message.obtain(null, MSG_NOTIFY)
- try {
- s.send(m)
- } catch (e: RemoteException) {
- if (rm == null) {
- rm = LinkedList<Messenger>()
- }
- rm.add(s)
- subscribers.remove(s)
- }
- }
- if (rm != null) {
- for (s in rm) {
- subscribers.remove(s)
- }
- }
+ sendNotify()
}
"tunnelHttp" -> {
Log.v(TAG, "got http tunnel request!")
@@ -280,6 +276,10 @@ class WalletBackendService : Service() {
when (operation) {
"init" -> {
Log.v(TAG, "got response for init operation")
+ sendNotify()
+ }
+ "reset" -> {
+ exitProcess(1)
}
else -> {
val id = message.getInt("id")
@@ -298,6 +298,7 @@ class WalletBackendService : Service() {
b.putString("response", "{}")
}
b.putInt("requestID", rd.clientRequestID)
+ b.putString("operation", operation)
rd.messenger.send(m)
}
}
diff --git a/app/src/main/java/net/taler/wallet/crypto/Encoding.kt b/app/src/main/java/net/taler/wallet/crypto/Encoding.kt
new file mode 100644
index 0000000..f64dcae
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/crypto/Encoding.kt
@@ -0,0 +1,116 @@
+package net.taler.wallet.crypto
+
+import java.io.ByteArrayOutputStream
+
+class EncodingException : Exception("Invalid encoding")
+
+
+object Base32Crockford {
+
+ private fun ByteArray.getIntAt(index: Int): Int {
+ val x = this[index].toInt()
+ return if (x >= 0) x else (x + 256)
+ }
+
+ private var encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
+
+ fun encode(data: ByteArray): String {
+ val sb = StringBuilder()
+ val size = data.size
+ var bitBuf = 0
+ var numBits = 0
+ var pos = 0
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ val d = data.getIntAt(pos++)
+ bitBuf = (bitBuf shl 8) or d
+ numBits += 8
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf shl (5 - numBits)
+ numBits = 5
+ }
+ val v = bitBuf.ushr(numBits - 5) and 31
+ sb.append(encTable[v])
+ numBits -= 5
+ }
+ return sb.toString();
+ }
+
+ fun decode(encoded: String, out: ByteArrayOutputStream) {
+ val size = encoded.length
+ var bitpos = 0
+ var bitbuf = 0
+ var readPosition = 0
+
+ while (readPosition < size || bitpos > 0) {
+ //println("at position $readPosition with bitpos $bitpos")
+ if (readPosition < size) {
+ val v = getValue(encoded[readPosition++])
+ bitbuf = (bitbuf shl 5) or v
+ bitpos += 5
+ }
+ while (bitpos >= 8) {
+ val d = (bitbuf ushr (bitpos - 8)) and 0xFF
+ out.write(d)
+ bitpos -= 8
+ }
+ if (readPosition == size && bitpos > 0) {
+ bitbuf = (bitbuf shl (8 - bitpos)) and 0xFF
+ bitpos = if (bitbuf == 0) 0 else 8
+ }
+ }
+ }
+
+ fun decode(encoded: String): ByteArray {
+ val out = ByteArrayOutputStream()
+ decode(encoded, out)
+ return out.toByteArray()
+ }
+
+ private fun getValue(chr: Char): Int {
+ var a = chr
+ when (a) {
+ 'O', 'o' -> a = '0'
+ 'i', 'I', 'l', 'L' -> a = '1'
+ 'u', 'U' -> a = 'V'
+ }
+ if (a in '0'..'9')
+ return a - '0'
+ if (a in 'a'..'z')
+ a = Character.toUpperCase(a)
+ var dec = 0
+ if (a in 'A'..'Z') {
+ if ('I' < a) dec++
+ if ('L' < a) dec++
+ if ('O' < a) dec++
+ if ('U' < a) dec++
+ return a - 'A' + 10 - dec
+ }
+ throw EncodingException()
+ }
+
+ /**
+ * Compute the length of the resulting string when encoding data of the given size
+ * in bytes.
+ *
+ * @param dataSize size of the data to encode in bytes
+ * @return size of the string that would result from encoding
+ */
+ fun calculateEncodedStringLength(dataSize: Int): Int {
+ return (dataSize * 8 + 4) / 5
+ }
+
+ /**
+ * Compute the length of the resulting data in bytes when decoding a (valid) string of the
+ * given size.
+ *
+ * @param stringSize size of the string to decode
+ * @return size of the resulting data in bytes
+ */
+ fun calculateDecodedDataLength(stringSize: Int): Int {
+ return stringSize * 5 / 8
+ }
+}
+
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index 5d9ac0a..a3aa93f 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -36,7 +36,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:text="0.6.0pre3 (Sat 02 Nov 2019)" />
+ android:text="0.6.0pre4 (Tue 03 Dec 2019)" />
</LinearLayout>
@@ -59,7 +59,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:text="0.6.0pre3 (Sat 02 Nov 2019, 70a2322940)" />
+ android:text="0.6.0pre4 (Tue 3 Dec 2019, 829acdd3d9)" />
</LinearLayout>
diff --git a/app/src/main/res/layout/fragment_show_balance.xml b/app/src/main/res/layout/fragment_show_balance.xml
index b93d2c2..57f9309 100644
--- a/app/src/main/res/layout/fragment_show_balance.xml
+++ b/app/src/main/res/layout/fragment_show_balance.xml
@@ -42,5 +42,18 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button_pay_qr"/>
+
+ <TextView
+ android:id="@+id/pending_operations_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Pending Operations:" />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_pending"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"/>
+
</LinearLayout>
</androidx.core.widget.NestedScrollView>
diff --git a/app/src/main/res/layout/fragment_show_history.xml b/app/src/main/res/layout/fragment_show_history.xml
index d730425..4ed11ac 100644
--- a/app/src/main/res/layout/fragment_show_history.xml
+++ b/app/src/main/res/layout/fragment_show_history.xml
@@ -1,13 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".WalletHistory">
+<androidx.core.widget.NestedScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="15dp">
- <TextView
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:text="The history of wallet operations will eventually be shown here. Currently this feature isn't implemented, sorry!"/>
+ android:orientation="vertical">
-</FrameLayout> \ No newline at end of file
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_history"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"/>
+
+ <TextView
+ android:id="@+id/list_history_placeholder"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="The wallet history is empty"
+ tools:visibility="gone"/>
+ </LinearLayout>
+
+</androidx.core.widget.NestedScrollView> \ No newline at end of file
diff --git a/app/src/main/res/layout/history_row.xml b/app/src/main/res/layout/history_row.xml
new file mode 100644
index 0000000..864baa1
--- /dev/null
+++ b/app/src/main/res/layout/history_row.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/history_text"
+ android:textSize="24sp" tools:text="My History Event">
+ </TextView>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/pending_row.xml b/app/src/main/res/layout/pending_row.xml
new file mode 100644
index 0000000..088ec99
--- /dev/null
+++ b/app/src/main/res/layout/pending_row.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/pending_text"
+ android:textSize="24sp" tools:text="My Pending Operation">
+ </TextView>
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/menu/balance.xml b/app/src/main/res/menu/balance.xml
new file mode 100644
index 0000000..f1565b1
--- /dev/null
+++ b/app/src/main/res/menu/balance.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:id="@+id/reload_balance"
+ android:title="Reload Balance"
+ android:orderInCategory="100"
+ app:showAsAction="never"/>
+ <item android:id="@+id/retry_pending"
+ android:title="Retry Pending Operations"
+ android:orderInCategory="100"
+ app:showAsAction="never"/>
+</menu>
diff --git a/app/src/main/res/menu/history.xml b/app/src/main/res/menu/history.xml
new file mode 100644
index 0000000..c8fb3e7
--- /dev/null
+++ b/app/src/main/res/menu/history.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:id="@+id/reload_history"
+ android:title="Reload History"
+ android:orderInCategory="100"
+ app:showAsAction="never"/>
+</menu>
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index e85427a..fac49f3 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -8,7 +8,7 @@
<fragment
android:id="@+id/showBalance"
android:name="net.taler.wallet.ShowBalance"
- android:label="Balances"
+ android:label="Home"
tools:layout="@layout/fragment_show_balance">
<action
android:id="@+id/action_showBalance_to_promptPayment"
diff --git a/app/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt b/app/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt
new file mode 100644
index 0000000..b5d2772
--- /dev/null
+++ b/app/src/test/java/net/taler/wallet/crypto/Base32CrockfordTest.kt
@@ -0,0 +1,20 @@
+package net.taler.wallet.crypto
+
+import org.junit.Assert.*
+import org.junit.Test
+
+class Base32CrockfordTest {
+ @Test
+ fun testBasic() {
+ val inputStr = "Hello, World"
+ val data = inputStr.toByteArray(Charsets.UTF_8)
+ val enc = Base32Crockford.encode(data)
+ println(enc)
+ val dec = Base32Crockford.decode(enc)
+ val recoveredInputStr = dec.toString(Charsets.UTF_8)
+ println(recoveredInputStr)
+
+ val foo = Base32Crockford.decode("51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30H2365338E9G6RT4AH1N6H13EGHR70RK6H1S6X2M4CSP8CSK8E1G88VKJH25610KGCHR8RWM4DJ47123CH9K89334D1S8N24ACJ48CR3EH256MR3AH1R711KCE9N6S134GSN6RW46D1H6CV3CDHJ6D0KEDHR6D24CD248MWKADHJ6WT34D25712KCD2474V46EA18H2M4GHM6WTK2E216S14CD238GSK0G9G692KCDHM6RW34CT16MV3CG9P60S34C1G70SMCHHQ8CVKJG9K6CVK6GHK70R46HJ26CR4AE9M8523ADHS8RR3EE1R74S32CHP6N1K0GT38D1M6C1R84TM2E9N8MSK2C1J71248E9H6H1MCD9J70VK4GSG6124CCHR6RS4ADSH8N0M4H1G84R4CD1G8D24AG9N6RR48DT1712K6GJ26X232DT36N0K4C9M8H236HJ48N2K4G9H8GVM8E1P8GSM6E9K891K4CSN65348C26611M8DHJ8S1M6H9G8H338CHS6GV3CD9K64S3GCHR8H2M6GJ58MT3EHA26S232GSJ6GTMAGA570W44DA2852KEDSR8MTKEGA460T3CCT18MR48CHK6WWKEGJ460WK4EA568VM6GSJ70T32CA461234DJ66RS34DHM6D242CT46MV3JDA584S4ADSM6S1MAE1P6GTKEGA68N1M8E216WRMAGHM6RR4ADSJ8MR3EDJ2690KAD9H6H346D9R88RKECSN8RRKJC1N74W34DSQ60W48DSJ8S1K0DSH8D1M4E1J6H1M2D1S8S33CG9R6RSMCH9K4CMGM81051JJ08SG64R30C1H4CMGM81054520A8A00")
+ println(foo.toString(Charsets.UTF_8))
+ }
+}