gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-android] branch master updated (30980bc -> 9a707c2)


From: gnunet
Subject: [taler-wallet-android] branch master updated (30980bc -> 9a707c2)
Date: Thu, 05 Mar 2020 16:49:35 +0100

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a change to branch master
in repository wallet-android.

    from 30980bc  Clean up and improve withdraw UI (first pass)
     new 1bbc48f  Move pending operations into their own fragment
     new 8589d5c  Make initial screen look nicer and clean up its code
     new 0a070b2  Add provisional dev mode checkbox to hide UI elements by 
default
     new 9a707c2  Add wallet to nightly F-Droid repo

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .gitlab-ci.yml                                     |  46 +++
 app/src/main/java/net/taler/wallet/MainActivity.kt |  11 +-
 app/src/main/java/net/taler/wallet/ShowBalance.kt  | 319 ++++++---------------
 .../main/java/net/taler/wallet/WalletViewModel.kt  | 103 ++-----
 .../net/taler/wallet/backend/WalletBackendApi.kt   |  27 +-
 .../wallet/pending/PendingOperationsFragment.kt    | 180 ++++++++++++
 .../wallet/pending/PendingOperationsManager.kt     |  64 +++++
 .../net/taler/wallet/withdraw/WithdrawManager.kt   |   2 +-
 app/src/main/res/drawable/ic_scan_qr.xml           | 208 +-------------
 app/src/main/res/layout/app_bar_main.xml           |   9 +-
 app/src/main/res/layout/balance_row.xml            |   2 +-
 ...payment.xml => fragment_pending_operations.xml} |  22 +-
 app/src/main/res/layout/fragment_show_balance.xml  | 122 ++++----
 app/src/main/res/menu/activity_main_drawer.xml     |   4 +
 app/src/main/res/menu/balance.xml                  |   7 +-
 .../menu/{balance.xml => pending_operations.xml}   |   5 -
 app/src/main/res/navigation/nav_graph.xml          |  12 +-
 app/src/main/res/values/strings.xml                |  10 +-
 nightly-stats.patch                                |  38 +++
 19 files changed, 583 insertions(+), 608 deletions(-)
 create mode 100644 .gitlab-ci.yml
 create mode 100644 
app/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
 create mode 100644 
app/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
 copy app/src/main/res/layout/{fragment_prompt_payment.xml => 
fragment_pending_operations.xml} (65%)
 copy app/src/main/res/menu/{balance.xml => pending_operations.xml} (85%)
 create mode 100644 nightly-stats.patch

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..f4bfc45
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,46 @@
+image: registry.gitlab.com/fdroid/ci-images-client:latest
+
+cache:
+  paths:
+    - .gradle/wrapper
+    - .gradle/caches
+
+stages:
+  - test
+  - deploy
+
+test:
+  stage: test
+  script: ./gradlew lint assembleRelease
+
+deploy_nightly:
+  stage: deploy
+  only:
+    - master
+  script:
+    # Ensure that key exists
+    - test -z "$DEBUG_KEYSTORE" && exit 0
+    # Rename nightly app
+    - sed -i
+      's,<string name="app_name">.*</string>,<string name="app_name">Taler 
Wallet Nightly</string>,'
+      app/src/main/res/values*/strings.xml
+    # Set time-based version code
+    - export versionCode=$(date '+%s')
+    - sed -i "s,^\(\s*versionCode\) *[0-9].*,\1 $versionCode," app/build.gradle
+    # Add commit to version name
+    - export versionName=$(git rev-parse --short=7 HEAD)
+    - sed -i "s,^\(\s*versionName\ *\"[0-9].*\)\",\1 ($versionName)\"," 
app/build.gradle
+    # Set nightly application ID
+    - sed -i "s,^\(\s*applicationId\) \"*[a-z\.].*\",\1 
\"net.taler.wallet.nightly\"," app/build.gradle
+    # Build the APK
+    - ./gradlew assembleDebug
+    # START only needed while patch not accepted/released upstream
+    - apt update && apt install patch
+    - patch /usr/lib/python3/dist-packages/fdroidserver/nightly.py 
nightly-stats.patch
+    # END
+    - CI_PROJECT_URL="https://gitlab.com/gnu-taler/fdroid-repo"; 
CI_PROJECT_PATH="gnu-taler/fdroid-repo" fdroid nightly -v
+
+after_script:
+  # this file changes every time but should not be cached
+  - rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
+  - rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
diff --git a/app/src/main/java/net/taler/wallet/MainActivity.kt 
b/app/src/main/java/net/taler/wallet/MainActivity.kt
index ebc7136..bca5e33 100644
--- a/app/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/app/src/main/java/net/taler/wallet/MainActivity.kt
@@ -24,7 +24,7 @@ import android.content.IntentFilter
 import android.os.Bundle
 import android.util.Log
 import android.view.MenuItem
-import android.view.View.GONE
+import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
 import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
@@ -69,12 +69,16 @@ class MainActivity : AppCompatActivity(), 
OnNavigationItemSelectedListener,
 
         setSupportActionBar(toolbar)
         val appBarConfiguration = AppBarConfiguration(
-            setOf(R.id.showBalance, R.id.settings, R.id.walletHistory), 
drawer_layout
+            setOf(R.id.showBalance, R.id.settings, R.id.walletHistory, 
R.id.nav_pending_operations), drawer_layout
         )
         toolbar.setupWithNavController(nav, appBarConfiguration)
 
         model.showProgressBar.observe(this, Observer { show ->
-            progress_bar.visibility = if (show) VISIBLE else GONE
+            progress_bar.visibility = if (show) VISIBLE else INVISIBLE
+        })
+
+        model.devMode.observe(this, Observer { enabled ->
+            nav_view.menu.findItem(R.id.nav_pending_operations).isVisible = 
enabled
         })
 
         if (intent.action == ACTION_VIEW) intent.dataString?.let { uri ->
@@ -99,6 +103,7 @@ class MainActivity : AppCompatActivity(), 
OnNavigationItemSelectedListener,
             R.id.nav_home -> nav.navigate(R.id.showBalance)
             R.id.nav_settings -> nav.navigate(R.id.settings)
             R.id.nav_history -> nav.navigate(R.id.walletHistory)
+            R.id.nav_pending_operations -> 
nav.navigate(R.id.nav_pending_operations)
         }
         drawer_layout.closeDrawer(START)
         return true
diff --git a/app/src/main/java/net/taler/wallet/ShowBalance.kt 
b/app/src/main/java/net/taler/wallet/ShowBalance.kt
index 4b52426..71ef87f 100644
--- a/app/src/main/java/net/taler/wallet/ShowBalance.kt
+++ b/app/src/main/java/net/taler/wallet/ShowBalance.kt
@@ -18,6 +18,7 @@ package net.taler.wallet
 
 import android.annotation.SuppressLint
 import android.os.Bundle
+import android.transition.TransitionManager.beginDelayedTransition
 import android.util.Log
 import android.view.LayoutInflater
 import android.view.Menu
@@ -27,141 +28,26 @@ import android.view.View
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import android.view.ViewGroup
-import android.widget.Button
-import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.snackbar.Snackbar
+import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.google.zxing.integration.android.IntentIntegrator
 import com.google.zxing.integration.android.IntentIntegrator.QR_CODE_TYPES
-import org.json.JSONObject
+import kotlinx.android.synthetic.main.fragment_show_balance.*
+import net.taler.wallet.BalanceAdapter.BalanceViewHolder
 
-class WalletBalanceAdapter(private var myDataset: WalletBalances) :
-    RecyclerView.Adapter<WalletBalanceAdapter.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 amount = myDataset.byCurrency[position].available
-        val amountIncoming = myDataset.byCurrency[position].pendingIncoming
-        val currencyView = 
holder.rowView.findViewById<TextView>(R.id.balance_currency)
-        currencyView.text = amount.currency
-        val amountView = 
holder.rowView.findViewById<TextView>(R.id.balance_amount)
-        amountView.text = amount.amount
-
-        val amountIncomingRow = 
holder.rowView.findViewById<View>(R.id.balance_row_pending)
-
-        val amountIncomingView = 
holder.rowView.findViewById<TextView>(R.id.balance_pending)
-        if (amountIncoming.isZero()) {
-            amountIncomingRow.visibility = GONE
-        } else {
-            amountIncomingRow.visibility = VISIBLE
-            @SuppressLint("SetTextI18n")
-            amountIncomingView.text = "${amountIncoming.amount} 
${amountIncoming.currency}"
-        }
-    }
-
-    fun update(updatedBalances: WalletBalances) {
-        this.myDataset = updatedBalances
-        this.notifyDataSetChanged()
-    }
-
-    class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
-}
-
-class PendingOperationsAdapter(private var myDataset: PendingOperations) :
-    RecyclerView.Adapter<PendingOperationsAdapter.MyViewHolder>() {
-
-    private var listener: PendingOperationClickListener? = null
-
-
-    init {
-        setHasStableIds(false)
-    }
-
-    fun setPendingOperationClickListener(listener: 
PendingOperationClickListener) {
-        this.listener = listener
-    }
-
-    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 pendingContainer = 
holder.rowView.findViewById<LinearLayout>(R.id.pending_container)
-        pendingContainer.setOnClickListener {
-            this.listener?.onPendingOperationClick(p.type, p.detail)
-        }
-        when (p.type) {
-            "proposal-choice" -> {
-                val btn1 = 
holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
-                btn1.text = 
btn1.context.getString(R.string.pending_operations_refuse)
-                btn1.visibility = VISIBLE
-                btn1.setOnClickListener {
-                    this.listener?.onPendingOperationActionClick(p.type, 
p.detail)
-                }
-            }
-            else -> {
-                val btn1 = 
holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
-                btn1.text = 
btn1.context.getString(R.string.pending_operations_no_action)
-                btn1.visibility = GONE
-                btn1.setOnClickListener {}
-            }
-        }
-        val textView = holder.rowView.findViewById<TextView>(R.id.pending_text)
-        val subTextView = 
holder.rowView.findViewById<TextView>(R.id.pending_subtext)
-        subTextView.text = p.detail.toString(1)
-        textView.text = p.type
-    }
-
-    fun update(updatedDataset: PendingOperations) {
-        this.myDataset = updatedDataset
-        this.notifyDataSetChanged()
-    }
-
-    class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
-}
-
-interface PendingOperationClickListener {
-    fun onPendingOperationClick(type: String, detail: JSONObject)
-    fun onPendingOperationActionClick(type: String, detail: JSONObject)
-}
-
-class ShowBalance : Fragment(), PendingOperationClickListener {
+class ShowBalance : Fragment() {
 
     private val model: WalletViewModel by activityViewModels()
     private val withdrawManager by lazy { model.withdrawManager }
 
-    private lateinit var pendingOperationsLabel: View
-    private lateinit var balancesView: RecyclerView
-    private lateinit var balancesPlaceholderView: TextView
-    private lateinit var balancesAdapter: WalletBalanceAdapter
-
-    private lateinit var pendingAdapter: PendingOperationsAdapter
+    private val balancesAdapter = BalanceAdapter()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -169,89 +55,60 @@ class ShowBalance : Fragment(), 
PendingOperationClickListener {
     }
 
     override fun onCreateView(
-        inflater: LayoutInflater, container: ViewGroup?,
+        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 {
-            IntentIntegrator(activity).apply {
-                setBeepEnabled(true)
-                setOrientationLocked(false)
-            }.initiateScan(QR_CODE_TYPES)
-        }
-
-        this.balancesView = view.findViewById(R.id.list_balances)
-        this.balancesPlaceholderView = 
view.findViewById(R.id.list_balances_placeholder)
-
-        val balances = model.balances.value!!
-
-        balancesAdapter = WalletBalanceAdapter(balances)
+        return inflater.inflate(R.layout.fragment_show_balance, container, 
false)
+    }
 
-        view.findViewById<RecyclerView>(R.id.list_balances).apply {
-            val myLayoutManager = LinearLayoutManager(context)
-            val myItemDecoration = DividerItemDecoration(context, 
myLayoutManager.orientation)
-            layoutManager = myLayoutManager
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        balancesList.apply {
+            layoutManager = LinearLayoutManager(context)
             adapter = balancesAdapter
-            addItemDecoration(myItemDecoration)
+            addItemDecoration(DividerItemDecoration(context, VERTICAL))
         }
 
-        updateBalances(balances)
-
         model.balances.observe(viewLifecycleOwner, Observer {
-            triggerLoading()
-            updateBalances(it)
+            onBalancesChanged(it)
         })
 
-
-        val withdrawTestkudosButton = 
view.findViewById<Button>(R.id.button_withdraw_testkudos)
-        withdrawTestkudosButton.setOnClickListener {
+        model.devMode.observe(viewLifecycleOwner, Observer { enabled ->
+            delayedTransition()
+            testWithdrawButton.visibility = if (enabled) VISIBLE else GONE
+        })
+        testWithdrawButton.setOnClickListener {
             withdrawManager.withdrawTestkudos()
         }
-
         withdrawManager.testWithdrawalInProgress.observe(viewLifecycleOwner, 
Observer { loading ->
             Log.v("taler-wallet", "observing balance loading $loading in show 
balance")
-            withdrawTestkudosButton.isEnabled = !loading
-            triggerLoading()
+            testWithdrawButton.isEnabled = !loading
+            model.showProgressBar.value = loading
         })
 
-        pendingAdapter = PendingOperationsAdapter(PendingOperations(listOf()))
-        pendingAdapter.setPendingOperationClickListener(this)
-
-        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)
+        scanButton.setOnClickListener {
+            IntentIntegrator(activity).apply {
+                setPrompt("")
+                setBeepEnabled(true)
+                setOrientationLocked(false)
+            }.initiateScan(QR_CODE_TYPES)
         }
-
-        model.pendingOperations.observe(viewLifecycleOwner, Observer {
-            updatePending(it)
-        })
-
-        return view
     }
 
-    override fun onResume() {
-        super.onResume()
-        triggerLoading()
-        Log.v("taler-wallet", "called onResume on ShowBalance")
+    override fun onStart() {
+        super.onStart()
+        model.loadBalances()
     }
 
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
         return when (item.itemId) {
-            R.id.retry_pending -> {
-                model.retryPendingNow()
+            R.id.reload_balance -> {
+                model.loadBalances()
                 true
             }
-            R.id.reload_balance -> {
-                triggerLoading()
-                model.balances.value = WalletBalances(false, listOf())
-                model.getBalances()
+            R.id.developer_mode -> {
+                item.isChecked = !item.isChecked
+                model.devMode.value = item.isChecked
                 true
             }
             else -> super.onOptionsItemSelected(item)
@@ -260,64 +117,72 @@ class ShowBalance : Fragment(), 
PendingOperationClickListener {
 
     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
         inflater.inflate(R.menu.balance, menu)
+        menu.findItem(R.id.developer_mode).isChecked = model.devMode.value!!
         super.onCreateOptionsMenu(menu, inflater)
     }
 
-    private fun triggerLoading() {
-        val withdrawInProgress = 
withdrawManager.testWithdrawalInProgress.value == true
-        val balances = model.balances.value
-        val loading: Boolean = (withdrawInProgress) || (balances == null) || 
!balances.initialized
-        model.showProgressBar.value = loading
-    }
-
-    private fun updateBalances(balances: WalletBalances) {
-        if (!balances.initialized) {
-            balancesPlaceholderView.visibility = GONE
-            balancesView.visibility = GONE
-        } else if (balances.byCurrency.isEmpty()) {
-            balancesPlaceholderView.visibility = VISIBLE
-            balancesView.visibility = GONE
+    private fun onBalancesChanged(balances: List<BalanceItem>) {
+        delayedTransition()
+        if (balances.isEmpty()) {
+            balancesEmptyState.visibility = VISIBLE
+            balancesList.visibility = GONE
         } else {
-            balancesPlaceholderView.visibility = GONE
-            balancesView.visibility = VISIBLE
+            balancesAdapter.setItems(balances)
+            balancesEmptyState.visibility = GONE
+            balancesList.visibility = VISIBLE
         }
-        Log.v(TAG, "updating balances $balances")
-        balancesAdapter.update(balances)
     }
 
-    private fun updatePending(pendingOperations: PendingOperations) {
-        if (pendingOperations.pending.isEmpty()) {
-            pendingOperationsLabel.visibility = GONE
-        } else {
-            pendingOperationsLabel.visibility = VISIBLE
-        }
-        pendingAdapter.update(pendingOperations)
+    private fun delayedTransition() {
+        beginDelayedTransition(view as ViewGroup)
     }
 
-    override fun onPendingOperationClick(type: String, detail: JSONObject) {
-        val v = view ?: return
-        when {
-            else -> {
-                val bar = Snackbar.make(
-                    v,
-                    "No detail view for $type implemented yet.",
-                    Snackbar.LENGTH_SHORT
-                )
-                bar.show()
-            }
-        }
+}
+
+class BalanceAdapter : Adapter<BalanceViewHolder>() {
+
+    private var items = emptyList<BalanceItem>()
+
+    init {
+        setHasStableIds(false)
     }
 
-    override fun onPendingOperationActionClick(type: String, detail: 
JSONObject) {
-        when (type) {
-            "proposal-choice" -> {
-                Log.v(TAG, "got action click on proposal-choice")
-                val proposalId = detail.optString("proposalId", "")
-                if (proposalId == "") {
-                    return
-                }
-                model.paymentManager.abortProposal(proposalId)
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 
BalanceViewHolder {
+        val v = 
LayoutInflater.from(parent.context).inflate(R.layout.balance_row, parent, false)
+        return BalanceViewHolder(v)
+    }
+
+    override fun getItemCount() = items.size
+
+    override fun onBindViewHolder(holder: BalanceViewHolder, position: Int) {
+        val item = items[position]
+        holder.bind(item)
+    }
+
+    fun setItems(items: List<BalanceItem>) {
+        this.items = items
+        this.notifyDataSetChanged()
+    }
+
+    class BalanceViewHolder(v: View) : ViewHolder(v) {
+        private val currencyView: TextView = 
v.findViewById(R.id.balance_currency)
+        private val amountView: TextView = v.findViewById(R.id.balance_amount)
+        private val amountIncomingRow: View = 
v.findViewById(R.id.balance_row_pending)
+        private val amountIncomingView: TextView = 
v.findViewById(R.id.balance_pending)
+
+        fun bind(item: BalanceItem) {
+            currencyView.text = item.available.currency
+            amountView.text = item.available.amount
+
+            val amountIncoming = item.pendingIncoming
+            if (amountIncoming.isZero()) {
+                amountIncomingRow.visibility = GONE
+            } else {
+                amountIncomingRow.visibility = VISIBLE
+                @SuppressLint("SetTextI18n")
+                amountIncomingView.text = "${amountIncoming.amount} 
${amountIncoming.currency}"
             }
         }
     }
+
 }
diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt 
b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
index d9e730d..1126ced 100644
--- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt
+++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
@@ -23,6 +23,7 @@ import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.asLiveData
+import androidx.lifecycle.distinctUntilChanged
 import androidx.lifecycle.switchMap
 import 
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
 import com.fasterxml.jackson.databind.ObjectMapper
@@ -37,37 +38,21 @@ import net.taler.wallet.backend.WalletBackendApi
 import net.taler.wallet.history.History
 import net.taler.wallet.history.HistoryEvent
 import net.taler.wallet.payment.PaymentManager
+import net.taler.wallet.pending.PendingOperationsManager
 import net.taler.wallet.withdraw.WithdrawManager
 import org.json.JSONObject
 
 const val TAG = "taler-wallet"
 
-
-data class BalanceEntry(val available: Amount, val pendingIncoming: Amount)
-
-
-data class WalletBalances(val initialized: Boolean, val byCurrency: 
List<BalanceEntry>)
-
-open class PendingOperationInfo(
-    val type: String,
-    val detail: JSONObject
-)
-
-open class PendingOperations(
-    val pending: List<PendingOperationInfo>
-)
-
+data class BalanceItem(val available: Amount, val pendingIncoming: Amount)
 
 @Suppress("EXPERIMENTAL_API_USAGE")
 class WalletViewModel(val app: Application) : AndroidViewModel(app) {
 
-    val balances = MutableLiveData<WalletBalances>().apply {
-        value = WalletBalances(false, listOf())
-    }
+    private val mBalances = MutableLiveData<List<BalanceItem>>()
+    val balances: LiveData<List<BalanceItem>> = 
mBalances.distinctUntilChanged()
 
-    val pendingOperations = MutableLiveData<PendingOperations>().apply {
-        value = PendingOperations(listOf())
-    }
+    val devMode = MutableLiveData(false)
 
     private val mHistoryProgress = MutableLiveData<Boolean>()
     val historyProgress: LiveData<Boolean> = mHistoryProgress
@@ -84,9 +69,16 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
     val showProgressBar = MutableLiveData<Boolean>()
 
     private var activeGetBalance = 0
-    private var activeGetPending = 0
 
-    private val walletBackendApi = WalletBackendApi(app)
+    private val walletBackendApi = WalletBackendApi(app, {
+        activeGetBalance = 0
+        loadBalances()
+        pendingOperationsManager.getPending()
+    }) {
+        Log.i(TAG, "Received notification from wallet-core")
+        loadBalances()
+        pendingOperationsManager.getPending()
+    }
 
     private val mapper = ObjectMapper()
         .registerModule(KotlinModule())
@@ -94,35 +86,27 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
 
     val withdrawManager = WithdrawManager(walletBackendApi)
     val paymentManager = PaymentManager(walletBackendApi, mapper)
+    val pendingOperationsManager: PendingOperationsManager =
+        PendingOperationsManager(walletBackendApi)
 
-    init {
-        getBalances()
-        getPending()
-
-        walletBackendApi.notificationHandler = {
-            Log.i(TAG, "got notification from wallet")
-            getBalances()
-            getPending()
-        }
-        walletBackendApi.connectedHandler = {
-            activeGetBalance = 0
-            activeGetPending = 0
-            getBalances()
-            getPending()
-        }
+    override fun onCleared() {
+        walletBackendApi.destroy()
+        super.onCleared()
     }
 
-    fun getBalances() {
+    @UiThread
+    fun loadBalances() {
         if (activeGetBalance > 0) {
             return
         }
         activeGetBalance++
+        showProgressBar.value = true
         walletBackendApi.sendRequest("getBalances", null) { isError, result ->
             activeGetBalance--
             if (isError) {
                 return@sendRequest
             }
-            val balanceList = mutableListOf<BalanceEntry>()
+            val balanceList = mutableListOf<BalanceItem>()
             val byCurrency = result.getJSONObject("byCurrency")
             val currencyList = byCurrency.keys().asSequence().toList().sorted()
             for (currency in currencyList) {
@@ -132,33 +116,10 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
                 val jsonAmountIncoming = byCurrency.getJSONObject(currency)
                     .getJSONObject("pendingIncoming")
                 val amountIncoming = Amount.fromJson(jsonAmountIncoming)
-                balanceList.add(BalanceEntry(amount, amountIncoming))
+                balanceList.add(BalanceItem(amount, amountIncoming))
             }
-            balances.postValue(WalletBalances(true, balanceList))
-        }
-    }
-
-    private fun getPending() {
-        if (activeGetPending > 0) {
-            return
-        }
-        activeGetPending++
-        walletBackendApi.sendRequest("getPendingOperations", null) { isError, 
result ->
-            activeGetPending--
-            if (isError) {
-                Log.i(TAG, "got getPending error result")
-                return@sendRequest
-            }
-            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)))
+            mBalances.postValue(balanceList)
+            showProgressBar.postValue(false)
         }
     }
 
@@ -189,7 +150,7 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
     fun dangerouslyReset() {
         walletBackendApi.sendRequest("reset", null)
         withdrawManager.testWithdrawalInProgress.value = false
-        balances.value = WalletBalances(false, listOf())
+        mBalances.value = emptyList()
     }
 
     fun startTunnel() {
@@ -205,12 +166,4 @@ class WalletViewModel(val app: Application) : 
AndroidViewModel(app) {
         walletBackendApi.sendRequest("tunnelResponse", respJson)
     }
 
-    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 8f37ff3..d447287 100644
--- a/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -22,21 +22,26 @@ import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
 import android.content.ServiceConnection
-import android.os.*
+import android.os.Handler
+import android.os.IBinder
+import android.os.Message
+import android.os.Messenger
 import android.util.Log
 import android.util.SparseArray
 import org.json.JSONObject
 import java.lang.ref.WeakReference
 import java.util.*
 
-class WalletBackendApi(private val app: Application) {
+class WalletBackendApi(
+    private val app: Application,
+    private val onConnected: (() -> Unit),
+    private val notificationHandler: (() -> Unit)
+) {
 
     private var walletBackendMessenger: Messenger? = null
     private val queuedMessages = LinkedList<Message>()
     private val handlers = SparseArray<(isError: Boolean, message: JSONObject) 
-> Unit>()
     private var nextRequestID = 1
-    var notificationHandler: (() -> Unit)? = null
-    var connectedHandler: (() -> Unit)? = null
 
     private val walletBackendConn = object : ServiceConnection {
         override fun onServiceDisconnected(p0: ComponentName?) {
@@ -52,15 +57,12 @@ class WalletBackendApi(private val app: Application) {
             val msg = Message.obtain(null, 
WalletBackendService.MSG_SUBSCRIBE_NOTIFY)
             msg.replyTo = incomingMessenger
             bm.send(msg)
-            val ch = connectedHandler
-            if (ch != null) {
-                ch()
-            }
+            onConnected.invoke()
         }
     }
 
     private class IncomingHandler(strongApi: WalletBackendApi) : Handler() {
-        private val weakApi = WeakReference<WalletBackendApi>(strongApi)
+        private val weakApi = WeakReference(strongApi)
         override fun handleMessage(msg: Message) {
             val api = weakApi.get() ?: return
             when (msg.what) {
@@ -83,10 +85,7 @@ class WalletBackendApi(private val app: Application) {
                     h(isError, json)
                 }
                 WalletBackendService.MSG_NOTIFY -> {
-                    val nh = api.notificationHandler
-                    if (nh != null) {
-                        nh()
-                    }
+                    api.notificationHandler.invoke()
                 }
             }
         }
@@ -139,4 +138,4 @@ class WalletBackendApi(private val app: Application) {
     companion object {
         const val TAG = "WalletBackendApi"
     }
-}
\ No newline at end of file
+}
diff --git 
a/app/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt 
b/app/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
new file mode 100644
index 0000000..946e5ba
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/pending/PendingOperationsFragment.kt
@@ -0,0 +1,180 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.pending
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+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.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_pending_operations.*
+import net.taler.wallet.R
+import net.taler.wallet.TAG
+import net.taler.wallet.WalletViewModel
+import org.json.JSONObject
+
+interface PendingOperationClickListener {
+    fun onPendingOperationClick(type: String, detail: JSONObject)
+    fun onPendingOperationActionClick(type: String, detail: JSONObject)
+}
+
+class PendingOperationsFragment : Fragment(), PendingOperationClickListener {
+
+    private val model: WalletViewModel by activityViewModels()
+    private val pendingOperationsManager by lazy { 
model.pendingOperationsManager }
+
+    private val pendingAdapter = PendingOperationsAdapter(emptyList(), this)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setHasOptionsMenu(true)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.fragment_pending_operations, 
container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        list_pending.apply {
+            val myLayoutManager = LinearLayoutManager(requireContext())
+            val myItemDecoration =
+                DividerItemDecoration(requireContext(), 
myLayoutManager.orientation)
+            layoutManager = myLayoutManager
+            adapter = pendingAdapter
+            addItemDecoration(myItemDecoration)
+        }
+
+        pendingOperationsManager.pendingOperations.observe(viewLifecycleOwner, 
Observer {
+            updatePending(it)
+        })
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
+            R.id.retry_pending -> {
+                pendingOperationsManager.retryPendingNow()
+                true
+            }
+            else -> super.onOptionsItemSelected(item)
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.pending_operations, menu)
+        super.onCreateOptionsMenu(menu, inflater)
+    }
+
+    private fun updatePending(pendingOperations: List<PendingOperationInfo>) {
+        pendingAdapter.update(pendingOperations)
+    }
+
+    override fun onPendingOperationClick(type: String, detail: JSONObject) {
+        Snackbar.make(view!!, "No detail view for $type implemented yet.", 
LENGTH_SHORT).show()
+    }
+
+    override fun onPendingOperationActionClick(type: String, detail: 
JSONObject) {
+        when (type) {
+            "proposal-choice" -> {
+                Log.v(TAG, "got action click on proposal-choice")
+                val proposalId = detail.optString("proposalId", "")
+                if (proposalId == "") {
+                    return
+                }
+                model.paymentManager.abortProposal(proposalId)
+            }
+        }
+    }
+
+}
+
+class PendingOperationsAdapter(
+    private var items: List<PendingOperationInfo>,
+    private val listener: PendingOperationClickListener
+) :
+    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 items.size
+    }
+
+    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
+        val p = items[position]
+        val pendingContainer = 
holder.rowView.findViewById<LinearLayout>(R.id.pending_container)
+        pendingContainer.setOnClickListener {
+            listener.onPendingOperationClick(p.type, p.detail)
+        }
+        when (p.type) {
+            "proposal-choice" -> {
+                val btn1 = 
holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
+                btn1.text = 
btn1.context.getString(R.string.pending_operations_refuse)
+                btn1.visibility = VISIBLE
+                btn1.setOnClickListener {
+                    listener.onPendingOperationActionClick(p.type, p.detail)
+                }
+            }
+            else -> {
+                val btn1 = 
holder.rowView.findViewById<TextView>(R.id.button_pending_action_1)
+                btn1.text = 
btn1.context.getString(R.string.pending_operations_no_action)
+                btn1.visibility = GONE
+                btn1.setOnClickListener {}
+            }
+        }
+        val textView = holder.rowView.findViewById<TextView>(R.id.pending_text)
+        val subTextView = 
holder.rowView.findViewById<TextView>(R.id.pending_subtext)
+        subTextView.text = p.detail.toString(1)
+        textView.text = p.type
+    }
+
+    fun update(items: List<PendingOperationInfo>) {
+        this.items = items
+        this.notifyDataSetChanged()
+    }
+
+    class MyViewHolder(val rowView: View) : RecyclerView.ViewHolder(rowView)
+
+}
diff --git 
a/app/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt 
b/app/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
new file mode 100644
index 0000000..2125dbc
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
@@ -0,0 +1,64 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.pending
+
+import android.util.Log
+import androidx.lifecycle.MutableLiveData
+import net.taler.wallet.TAG
+import net.taler.wallet.backend.WalletBackendApi
+import org.json.JSONObject
+
+open class PendingOperationInfo(
+    val type: String,
+    val detail: JSONObject
+)
+
+class PendingOperationsManager(private val walletBackendApi: WalletBackendApi) 
{
+
+    private var activeGetPending = 0
+
+    val pendingOperations = MutableLiveData<List<PendingOperationInfo>>()
+
+    internal fun getPending() {
+        if (activeGetPending > 0) {
+            return
+        }
+        activeGetPending++
+        walletBackendApi.sendRequest("getPendingOperations", null) { isError, 
result ->
+            activeGetPending--
+            if (isError) {
+                Log.i(TAG, "got getPending error result")
+                return@sendRequest
+            }
+            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((pendingList))
+        }
+    }
+
+    fun retryPendingNow() {
+        walletBackendApi.sendRequest("retryPendingNow", null)
+    }
+
+}
diff --git a/app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt 
b/app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index fa20318..dec03af 100644
--- a/app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/app/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -46,7 +46,7 @@ sealed class WithdrawStatus {
 class WithdrawManager(private val walletBackendApi: WalletBackendApi) {
 
     val withdrawStatus = MutableLiveData<WithdrawStatus>(WithdrawStatus.None)
-    val testWithdrawalInProgress = MutableLiveData<Boolean>(false)
+    val testWithdrawalInProgress = MutableLiveData(false)
 
     private var currentWithdrawRequestId = 0
 
diff --git a/app/src/main/res/drawable/ic_scan_qr.xml 
b/app/src/main/res/drawable/ic_scan_qr.xml
index a6d1172..2ca8a69 100644
--- a/app/src/main/res/drawable/ic_scan_qr.xml
+++ b/app/src/main/res/drawable/ic_scan_qr.xml
@@ -1,202 +1,10 @@
-<!--
-  ~ This file is part of GNU Taler
-  ~ (C) 2020 Taler Systems S.A.
-  ~
-  ~ GNU Taler is free software; you can redistribute it and/or modify it under 
the
-  ~ terms of the GNU General Public License as published by the Free Software
-  ~ Foundation; either version 3, or (at your option) any later version.
-  ~
-  ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT 
ANY
-  ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
-  ~ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-  ~
-  ~ You should have received a copy of the GNU General Public License along 
with
-  ~ GNU Taler; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
-  -->
-
 <vector xmlns:android="http://schemas.android.com/apk/res/android";
-        android:width="200dp"
-        android:height="200dp"
-        android:viewportWidth="278"
-        android:viewportHeight="278">
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M103,28l0,77 -76,0 0,-77 76,0zM93,38l-56,0 0,57 
56,0 0,-57z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M251,28l0,77 -76,0 0,-77 76,0zM241,38l-56,0 0,57 
56,0 0,-57z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M103,174l0,76 -76,0 0,-76 76,0zM93,184l-56,0 
0,56 56,0 0,-56z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            
android:pathData="M157,240l0,-12l-10,0l0,12l-12,0l0,-21l20,0l0,-10l-33,0l0,-9l-10,0l1,19l12,0l0,11l-13,0l0,10l13,0l0,10l42,0l0,-10z"
 />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M38,123l-11,0l0,-10l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M123,69l-10,0l0,-10l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M124,39l-11,0l0,-10l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M222,215l-10,0l0,-10l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            
android:pathData="M50,144l10,0l0,-10l-10,0l0,-10l-10,0l0,9l-12,0l0,24l12,0l0,8l31,0l0,-19l-11,0l0,9l-10,0z"
 />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M242,113l0,10 9,0 0,10 -16,0 0,13 -29,0 0,9 16,0 
0,23 -11,0 0,-13 -23,0 0,-10 8,0 0,-9 -6,0 0,-10 33,0 0,-18 -13,0 0,-5c10,0 
21,0 32,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            
android:pathData="M121,123l-31,0l0,10l-21,0l0,-10l-11,0l0,-10l63,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M123,74l-10,0l0,17l0,17l10,0l11,0l0,-17l-11,0z" 
/>
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M134,85l11,0l0,18l13,0l0,-12l8,0l0,-16l-32,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M153,44l-28,0 0,11 18,0 0,11 13,0 0,-11 
11,0c0,-9 0,-18 0,-27l-24,0 0,11 10,0 0,5z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M138,70l-10,0l0,-10l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M112,139l-10,0l0,-10l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M157,183l-11,0l0,-18l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M140,180l-10,0l0,-10l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M206,189l-10,0l0,-20l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M241,240l-18,0l0,10l28,0l0,-26l-10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M183,237l-11,0l0,-18l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M123,187l-10,0l0,-23l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            
android:pathData="M172,196l-15,0l0,-10l-32,0l0,10l23,0l0,10l12,0l0,11l46,0l0,-11l-13,0l0,-10l-11,0l0,10l-10,0z"
 />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M185,179l-13,0l0,11l-10,0l0,-21l23,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M198,220l10,0 0,20 9,0 0,10c-16,0 -13,0 
-31,0l0,-10 12,0 0,-20z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M118,155l-30,0l0,10l-10,0l0,-13l0,0l0,-7l40,0z" 
/>
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M136,123l0,9l21,0l0,-9l13,0l0,-10l-43,0l0,10z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M145,147l-22,0l0,-10l22,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M158,150l-11,0l0,10l22,0l0,-23l-11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M251,184l-6,0l0,8l-10,0l0,-8l-5,0l0,-22l21,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M222,194l-11,0l0,-10l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M228,217c7,0 15,0 23,0l0,-19 -10,0 0,9 -13,0 
0,10z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M237,236l-11,0l0,-10l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M82,82l-34,0l0,-32l34,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M230,82l-34,0l0,-32l34,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M82,228l-34,0l0,-32l34,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M183,250l-11,0l0,-10l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M140,162l-11,0l0,-10l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M218,132l-10,0l0,-10l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M193,123l-11,0l0,-10l11,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M251,159l-10,0l0,-10l10,0z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M0,53l0,-53l54,0l0,10l-44,0l0,43z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M278,53l0,-53l-54,0l0,10l44,0l0,43z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M0,225l0,53l54,0l0,-10l-44,0l0,-43z" />
-    <path
-            android:fillColor="#FFFFFF"
-            android:fillType="nonZero"
-            android:pathData="M278,225l0,53l-54,0l0,-10l44,0l0,-43z" />
+        android:width="24dp"
+        android:height="24dp"
+        android:tint="?attr/colorOnPrimarySurface"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+            android:fillColor="#000"
+            
android:pathData="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2
 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 
0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
 </vector>
diff --git a/app/src/main/res/layout/app_bar_main.xml 
b/app/src/main/res/layout/app_bar_main.xml
index e2fa71f..d976be8 100644
--- a/app/src/main/res/layout/app_bar_main.xml
+++ b/app/src/main/res/layout/app_bar_main.xml
@@ -43,13 +43,14 @@
             <me.zhanghai.android.materialprogressbar.MaterialProgressBar
                     android:id="@+id/progress_bar"
                     
style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal"
-                    android:layout_width="match_parent"
+                    android:layout_width="0dp"
                     android:layout_height="4dp"
+                    android:elevation="4dp"
                     android:indeterminate="true"
-                    android:visibility="gone"
-                    app:layout_constraintBottom_toBottomOf="parent"
+                    android:visibility="invisible"
+                    app:layout_constraintBottom_toBottomOf="@+id/toolbar"
+                    app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/toolbar"
                     app:mpb_progressStyle="horizontal"
                     app:mpb_useIntrinsicPadding="false"
                     tools:visibility="visible" />
diff --git a/app/src/main/res/layout/balance_row.xml 
b/app/src/main/res/layout/balance_row.xml
index 20ebf48..662068b 100644
--- a/app/src/main/res/layout/balance_row.xml
+++ b/app/src/main/res/layout/balance_row.xml
@@ -81,7 +81,7 @@
         <TextView
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="@string/balance_inbound"
+                android:text="@string/balances_inbound"
                 android:textColor="#006600" />
 
     </LinearLayout>
diff --git a/app/src/main/res/layout/fragment_prompt_payment.xml 
b/app/src/main/res/layout/fragment_pending_operations.xml
similarity index 65%
copy from app/src/main/res/layout/fragment_prompt_payment.xml
copy to app/src/main/res/layout/fragment_pending_operations.xml
index 26cbeb6..26c1be1 100644
--- a/app/src/main/res/layout/fragment_prompt_payment.xml
+++ b/app/src/main/res/layout/fragment_pending_operations.xml
@@ -18,27 +18,17 @@
         xmlns:app="http://schemas.android.com/apk/res-auto";
         xmlns:tools="http://schemas.android.com/tools";
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        tools:context=".payment.PromptPaymentFragment">
+        android:layout_height="match_parent">
 
-    <include
-            android:id="@+id/scrollView"
-            layout="@layout/payment_details"
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/list_pending"
             android:layout_width="0dp"
             android:layout_height="0dp"
-            app:layout_constraintBottom_toTopOf="@+id/bottomView"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-    <include
-            android:id="@+id/bottomView"
-            layout="@layout/payment_bottom_bar"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
+            android:scrollbars="vertical"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/scrollView" />
+            app:layout_constraintTop_toTopOf="parent"
+            tools:listitem="@layout/pending_row" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/layout/fragment_show_balance.xml 
b/app/src/main/res/layout/fragment_show_balance.xml
index af003eb..5b38fb6 100644
--- a/app/src/main/res/layout/fragment_show_balance.xml
+++ b/app/src/main/res/layout/fragment_show_balance.xml
@@ -13,64 +13,80 @@
   ~ You should have received a copy of the GNU General Public License along 
with
   ~ GNU Taler; see the file COPYING.  If not, see 
<http://www.gnu.org/licenses/>
   -->
-
-<androidx.core.widget.NestedScrollView 
xmlns:android="http://schemas.android.com/apk/res/android";
+<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android";
+        xmlns:app="http://schemas.android.com/apk/res-auto";
         xmlns:tools="http://schemas.android.com/tools";
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_margin="15dp">
-
-    <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="vertical">
-
-        <View
-                android:id="@+id/header"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
-
-        <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/list_balances"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:scrollbars="vertical" />
-
-        <TextView
-                android:id="@+id/list_balances_placeholder"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                android:text="@string/balance_no_cash"
-                tools:visibility="gone" />
+        android:layout_height="match_parent">
 
-        <Space
-                android:layout_width="match_parent"
-                android:layout_height="20dp" />
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/balancesList"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:visibility="gone"
+            app:layout_constraintBottom_toTopOf="@+id/scanButton"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_bias="0.0"
+            app:layout_constraintVertical_chainStyle="packed"
+            tools:layout_height="200dp"
+            tools:listitem="@layout/balance_row"
+            tools:visibility="visible" />
 
-        <Button
-                android:id="@+id/button_withdraw_testkudos"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/withdraw_button_testkudos" />
+    <TextView
+            android:id="@+id/balancesEmptyState"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:autoLink="web"
+            android:gravity="center"
+            android:padding="16dp"
+            android:text="@string/balances_empty_state"
+            android:textSize="18sp"
+            android:visibility="gone"
+            app:layout_constraintBottom_toTopOf="@+id/scanButton"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:visibility="visible" />
 
-        <Button
-                android:id="@+id/button_pay_qr"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/button_scan_qr_code" />
+    <androidx.constraintlayout.widget.Barrier
+            android:id="@+id/barrier"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:barrierAllowsGoneWidgets="false"
+            app:barrierDirection="bottom"
+            app:constraint_referenced_ids="balancesList, balancesEmptyState" />
 
-        <TextView
-                android:id="@+id/pending_operations_label"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:text="@string/pending_operations_label" />
+    <Button
+            android:id="@+id/scanButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:drawableLeft="@drawable/ic_scan_qr"
+            android:padding="16dp"
+            android:text="@string/button_scan_qr_code"
+            app:layout_constraintBottom_toTopOf="@+id/testWithdrawButton"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/barrier"
+            app:layout_constraintVertical_chainStyle="packed"
+            tools:ignore="RtlHardcoded" />
 
-        <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/list_pending"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:scrollbars="vertical" />
+    <Button
+            android:id="@+id/testWithdrawButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginTop="16dp"
+            android:layout_marginEnd="16dp"
+            android:padding="16dp"
+            android:text="@string/withdraw_button_testkudos"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/scanButton"
+            tools:visibility="visible" />
 
-    </LinearLayout>
-</androidx.core.widget.NestedScrollView>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/src/main/res/menu/activity_main_drawer.xml 
b/app/src/main/res/menu/activity_main_drawer.xml
index f06f0ab..7a54077 100644
--- a/app/src/main/res/menu/activity_main_drawer.xml
+++ b/app/src/main/res/menu/activity_main_drawer.xml
@@ -31,6 +31,10 @@
                 android:id="@+id/nav_settings"
                 android:icon="@drawable/ic_menu_manage"
                 android:title="@string/menu_settings" />
+        <item
+                android:id="@+id/nav_pending_operations"
+                android:icon="@drawable/history_refresh"
+                android:title="Pending Operations" />
     </group>
 
 </menu>
diff --git a/app/src/main/res/menu/balance.xml 
b/app/src/main/res/menu/balance.xml
index 15a6016..7ac3a9f 100644
--- a/app/src/main/res/menu/balance.xml
+++ b/app/src/main/res/menu/balance.xml
@@ -18,12 +18,11 @@
         xmlns:app="http://schemas.android.com/apk/res-auto";>
     <item
             android:id="@+id/reload_balance"
-            android:orderInCategory="100"
             android:title="@string/menu_balance_reload"
             app:showAsAction="never" />
     <item
-            android:id="@+id/retry_pending"
-            android:orderInCategory="100"
-            android:title="@string/menu_retry_pending_operations"
+            android:id="@+id/developer_mode"
+            android:checkable="true"
+            android:title="@string/menu_developer_mode"
             app:showAsAction="never" />
 </menu>
diff --git a/app/src/main/res/menu/balance.xml 
b/app/src/main/res/menu/pending_operations.xml
similarity index 85%
copy from app/src/main/res/menu/balance.xml
copy to app/src/main/res/menu/pending_operations.xml
index 15a6016..980ea66 100644
--- a/app/src/main/res/menu/balance.xml
+++ b/app/src/main/res/menu/pending_operations.xml
@@ -16,11 +16,6 @@
 
 <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:orderInCategory="100"
-            android:title="@string/menu_balance_reload"
-            app:showAsAction="never" />
     <item
             android:id="@+id/retry_pending"
             android:orderInCategory="100"
diff --git a/app/src/main/res/navigation/nav_graph.xml 
b/app/src/main/res/navigation/nav_graph.xml
index 2cc1eaa..648c88e 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -24,7 +24,7 @@
     <fragment
             android:id="@+id/showBalance"
             android:name="net.taler.wallet.ShowBalance"
-            android:label="Home"
+            android:label="@string/balances_title"
             tools:layout="@layout/fragment_show_balance">
         <action
                 android:id="@+id/action_showBalance_to_promptPayment"
@@ -99,8 +99,18 @@
                 app:popUpTo="@id/showBalance" />
     </fragment>
 
+    <fragment
+            android:id="@+id/nav_pending_operations"
+            android:name="net.taler.wallet.pending.PendingOperationsFragment"
+            android:label="Pending Operations"
+            tools:layout="@layout/fragment_pending_operations" />
+
     <action
             android:id="@+id/action_global_promptPayment"
             app:destination="@id/promptPayment" />
 
+    <action
+            android:id="@+id/action_global_pending_operations"
+            app:destination="@id/nav_pending_operations" />
+
 </navigation>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml 
b/app/src/main/res/values/strings.xml
index 19159b9..ca69756 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,11 +32,16 @@
     <string name="menu_history">History</string>
     <string name="menu_settings">Settings</string>
     <string name="menu_balance_reload">Reload balances</string>
+    <string name="menu_developer_mode">Developer Mode</string>
     <string name="menu_retry_pending_operations">Retry Pending 
Operations</string>
 
     <string name="servicedesc">my service</string>
     <string name="aiddescription">my aid</string>
 
+    <string name="balances_title">Balances</string>
+    <string name="balances_inbound">inbound</string>
+    <string name="balances_empty_state">There is no digital cash in your 
wallet.\n\nYou can get test money from the demo 
bank:\n\nhttps://bank.demo.taler.net</string>
+
     <!-- HistoryEvents -->
     <string name="history_event_exchange_added">Exchange Added</string>
     <string name="history_event_exchange_updated">Exchange Updated</string>
@@ -71,16 +76,13 @@
     <string name="payment_already_paid">You\'ve already paid for this 
order.</string>
 
     <string name="withdraw_accepted">Withdrawal accepted</string>
-    <string name="withdraw_success_info">Your bank will now ask you to approve 
a transfer to the selected change. After you\'ve confirmed the transfer with 
your bank, the digital cash will show in this wallet.</string>
+    <string name="withdraw_success_info">The wire transfer now needs to be 
confirmed with the bank. Once the wire transfer is complete, the digital cash 
will automatically show in this wallet.</string>
     <string name="withdraw_do_you_want">Do you want to withdraw</string>
     <string name="withdraw_fees">(minus exchange fees not shown in this 
prototype)</string>
     <string name="withdraw_exchange">Using the exchange provider</string>
     <string name="withdraw_button_testkudos">Withdraw TESTKUDOS</string>
     <string name="withdraw_button_confirm">Confirm Withdraw</string>
 
-    <string name="balance_inbound">inbound</string>
-    <string name="balance_no_cash">There is no digital cash in your 
wallet.</string>
-
     <string name="pending_operations_label">Pending Operations:</string>
     <string name="pending_operations_refuse">Refuse Proposal</string>
     <string name="pending_operations_no_action">(no action)</string>
diff --git a/nightly-stats.patch b/nightly-stats.patch
new file mode 100644
index 0000000..689f46a
--- /dev/null
+++ b/nightly-stats.patch
@@ -0,0 +1,38 @@
+diff --git a/fdroidserver/nightly.py b/fdroidserver/nightly.py
+index 0a3a8012..ae3aa0e3 100644
+--- a/fdroidserver/nightly.py
++++ b/fdroidserver/nightly.py
+@@ -170,6 +170,7 @@ def main():
+         git_mirror_path = os.path.join(repo_basedir, 'git-mirror')
+         git_mirror_repodir = os.path.join(git_mirror_path, 'fdroid', 'repo')
+         git_mirror_metadatadir = os.path.join(git_mirror_path, 'fdroid', 
'metadata')
++        git_mirror_statsdir = os.path.join(git_mirror_path, 'fdroid', 'stats')
+         if not os.path.isdir(git_mirror_repodir):
+             logging.debug(_('cloning {url}').format(url=clone_url))
+             try:
+@@ -217,6 +218,8 @@ Last updated: {date}'''.format(repo_git_base=repo_git_base,
+             common.local_rsync(options, git_mirror_repodir + '/', 'repo/')
+         if os.path.isdir(git_mirror_metadatadir):
+             common.local_rsync(options, git_mirror_metadatadir + '/', 
'metadata/')
++        if os.path.isdir(git_mirror_statsdir):
++            common.local_rsync(options, git_mirror_statsdir + '/', 'stats/')
+ 
+         ssh_private_key_file = _ssh_key_from_debug_keystore()
+         # this is needed for GitPython to find the SSH key
+@@ -246,7 +249,7 @@ Last updated: {date}'''.format(repo_git_base=repo_git_base,
+         config += "keydname = '%s'\n" % DISTINGUISHED_NAME
+         config += "make_current_version_link = False\n"
+         config += "accepted_formats = ('txt', 'yml')\n"
+-        # TODO add update_stats = True
++        config += "update_stats = True\n"
+         with open('config.py', 'w') as fp:
+             fp.write(config)
+         os.chmod('config.py', 0o600)
+@@ -293,6 +296,7 @@ Last updated: {date}'''.format(repo_git_base=repo_git_base,
+         subprocess.check_call(['fdroid', 'update', '--rename-apks', 
'--create-metadata', '--verbose'],
+                               cwd=repo_basedir)
+         common.local_rsync(options, repo_basedir + '/metadata/', 
git_mirror_metadatadir + '/')
++        common.local_rsync(options, repo_basedir + '/stats/', 
git_mirror_statsdir + '/')
+         mirror_git_repo.git.add(all=True)
+         mirror_git_repo.index.commit("update app metadata")
+ 

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]