Skip to content

Commit

Permalink
Android fixes (spesmilo#1117)
Browse files Browse the repository at this point in the history
* Fix FloatingActionButton positioning on API levels before 21

* Use pkgutil to load data files, so `electroncash` package doesn't need to be extracted on Android:
Closes spesmilo#1062, which I assume was caused by the cache directory being cleared while the app was running.

* Update to Chaquopy 5.0.7 (closes spesmilo#1060)

* Use new Chaquopy primitive conversion methods

* Use new Chaquopy container view methods

* Update to Chaquopy 5.1.0  /  Pin version of pycryptodomex

* Use correct container interface when calling get_blockchains

* Add missing lock (closes spesmilo#1109)

* Don't restore UI state if process has been restarted (closes spesmilo#1057, closes spesmilo#1093, closes spesmilo#1095)

* Fix "Can not perform this action after onSaveInstanceState" errors (closes spesmilo#1083, closes spesmilo#1091, closes spesmilo#1103)

* Trigger onShowDialog from DialogFragment.onStart rather than Dialog.setOnShowListener (closes spesmilo#1108, and see also spesmilo#1046)

* Don't show temporary files in wallet list (closes spesmilo#1102)

* ImageView size is now unavailable in onShowDialog, so get QR resolution from resource instead

* Catch network exceptions at top level of Connection thread (closes spesmilo#1096)

* Add missing locks

* Fix unit tests

* Update Android license agreement hash
  • Loading branch information
mhsmith authored and cculianu committed Jan 22, 2019
1 parent d519384 commit aaed660
Show file tree
Hide file tree
Showing 27 changed files with 211 additions and 161 deletions.
2 changes: 1 addition & 1 deletion android/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ RUN filename=sdk-tools-linux-4333796.zip && \

# Indicate that we accept the license which has the given hash.
RUN mkdir android-sdk/licenses && \
echo d56f5187479451eabf01fb78af6dfcb131a6481e > android-sdk/licenses/android-sdk-license
echo 24333f8a63b6825ea9c5514f83c2829b004d1fee > android-sdk/licenses/android-sdk-license

# make_locale
RUN apt-get update && \
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ android {
python {
pip {
install "-r", "$REPO_ROOT/contrib/deterministic-build/requirements.txt"
install "pycryptodomex"
install "pycryptodomex==3.6.6"
install "setuptools==40.6.3" // pkg_resources is used in android/console.py.
}
extractPackages "electroncash"
}
ndk {
abiFilters "x86", "armeabi-v7a"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class AddressesFragment : Fragment(), MainFragment {
daemonModel.addresses.observe(viewLifecycleOwner, Observer { addresses ->
rvAddresses.adapter =
if (addresses == null) null
else AddressesAdapter(activity!!, daemonModel.wallet!!, addresses)
else AddressesAdapter(activity!!, daemonModel.wallet!!, addresses.asList())

subtitle.value = when {
addresses == null -> getString(R.string.no_wallet)
Expand All @@ -78,15 +78,15 @@ class AddressesFragment : Fragment(), MainFragment {


class AddressesAdapter(val activity: FragmentActivity, val wallet: PyObject,
val addresses: PyObject)
val addresses: List<PyObject>)
: BoundAdapter<AddressModel>(R.layout.address) {

override fun getItem(position: Int): AddressModel {
return AddressModel(wallet, addresses.callAttr("__getitem__", position))
return AddressModel(wallet, addresses.get(position))
}

override fun getItemCount(): Int {
return addresses.callAttr("__len__").toJava(Int::class.java)
return addresses.size
}

override fun onBindViewHolder(holder: BoundViewHolder<AddressModel>, position: Int) {
Expand All @@ -99,7 +99,7 @@ class AddressesAdapter(val activity: FragmentActivity, val wallet: PyObject,

class AddressModel(val wallet: PyObject, val addr: PyObject) {
val type
get() = guiAddresses.callAttr("addr_type", wallet, addr).toJava(Int::class.java)
get() = guiAddresses.callAttr("addr_type", wallet, addr).toInt()

val addrString
get() = addr.callAttr("to_ui_string").toString()
Expand Down
26 changes: 10 additions & 16 deletions android/app/src/main/java/org/electroncash/electroncash3/Daemon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class DaemonModel {
// non-exceptionally.
watchdog = Runnable {
for (thread in listOf(daemon, network)) {
if (! thread.callAttr("is_alive").toJava(Boolean::class.java)) {
if (! thread.callAttr("is_alive").toBoolean()) {
throw RuntimeException("$thread unexpectedly stopped")
}
}
Expand All @@ -68,20 +68,21 @@ class DaemonModel {

fun initCallback() {
callback = Runnable {
if (network.callAttr("is_connected").toJava(Boolean::class.java)) {
if (network.callAttr("is_connected").toBoolean()) {
netStatus.value = NetworkStatus(
network.callAttr("get_local_height").toJava(Int::class.java),
network.callAttr("get_server_height").toJava(Int::class.java))
network.callAttr("get_local_height").toInt(),
network.callAttr("get_server_height").toInt())
} else {
netStatus.value = null
}

val wallet = this.wallet
if (wallet != null) {
walletName.value = wallet.callAttr("basename").toString()
if (wallet.callAttr("is_up_to_date").toJava(Boolean::class.java)) {
val balances = wallet.callAttr("get_balance") // Returns (confirmed, unconfirmed, unmatured)
walletBalance.value = balances.callAttr("__getitem__", 0).toJava(Long::class.java)
if (wallet.callAttr("is_up_to_date").toBoolean()) {
// get_balance returns the tuple (confirmed, unconfirmed, unmatured)
val balances = wallet.callAttr("get_balance").asList()
walletBalance.value = balances.get(0).toLong()
} else {
walletBalance.value = null
}
Expand Down Expand Up @@ -112,15 +113,8 @@ class DaemonModel {
}
}

// TODO remove once Chaquopy provides better syntax.
fun listWallets(): MutableList<String> {
val pyNames = commands.callAttr("list_wallets")
val names = ArrayList<String>()
for (i in 0 until pyNames.callAttr("__len__").toJava(Int::class.java)) {
val name = pyNames.callAttr("__getitem__", i).toString()
names.add(name)
}
return names
fun listWallets(): List<String> {
return commands.callAttr("list_wallets").asList().map { it.toString() }
}

fun createWallet(name: String, password: String, kwargName: String, kwargValue: String) {
Expand Down
17 changes: 14 additions & 3 deletions android/app/src/main/java/org/electroncash/electroncash3/Dialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,27 @@ import android.widget.PopupMenu


abstract class AlertDialogFragment : DialogFragment() {
var firstStart = true

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(context!!)
onBuildDialog(builder)
val dialog = builder.create()
dialog.setOnShowListener { onShowDialog(dialog) }
return dialog
return builder.create()
}

open fun onBuildDialog(builder: AlertDialog.Builder) {}

// We used to trigger onShowDialog from Dialog.setOnShowListener, but we had crash reports
// indicating that the fragment context was sometimes null in that listener (#1046, #1108).
// So use one of the fragment lifecycle methods instead.
override fun onStart() {
super.onStart()
if (firstStart) {
firstStart = false
onShowDialog(dialog as AlertDialog)
}
}

/** Can be used to do things like configure custom views, or attach listeners to buttons so
* they don't always close the dialog. */
open fun onShowDialog(dialog: AlertDialog) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fun formatFiatAmountAndUnit(amount: Long): String? {


fun formatFiatAmount(amount: Long): String? {
if (!fx.callAttr("is_enabled").toJava(Boolean::class.java)) {
if (!fx.callAttr("is_enabled").toBoolean()) {
return null
}
val amountStr = fx.callAttr("format_amount", amount).toString()
Expand Down
44 changes: 33 additions & 11 deletions android/app/src/main/java/org/electroncash/electroncash3/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.main.*
import kotlin.properties.Delegates.notNull
import kotlin.reflect.KClass


Expand All @@ -20,14 +21,18 @@ val FRAGMENTS = HashMap<Int, KClass<out Fragment>>().apply {


class MainActivity : AppCompatActivity() {
var stateValid: Boolean by notNull()
var cleanStart = true

override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme) // Remove splash screen.
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
cleanStart = savedInstanceState.getBoolean("cleanStart", true)
}
override fun onCreate(state: Bundle?) {
// Remove splash screen: doesn't work if called after super.onCreate.
setTheme(R.style.AppTheme)

// If the wallet name doesn't match, the process has probably been restarted, so
// ignore the UI state, including all dialogs.
stateValid = (state != null &&
(state.getString("walletName") == daemonModel.walletName.value))
super.onCreate(if (stateValid) state else null)

setContentView(R.layout.main)
navigation.setOnNavigationItemSelectedListener {
Expand All @@ -39,14 +44,28 @@ class MainActivity : AppCompatActivity() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean("cleanStart", cleanStart)
outState.putString("walletName", daemonModel.walletName.value)
}

override fun onRestoreInstanceState(state: Bundle) {
if (stateValid) {
super.onRestoreInstanceState(state)
cleanStart = state.getBoolean("cleanStart", true)
}
}

override fun onResume() {
super.onResume()
override fun onPostCreate(state: Bundle?) {
super.onPostCreate(if (stateValid) state else null)
}

override fun onResumeFragments() {
super.onResumeFragments()
showFragment(navigation.selectedItemId)
if (cleanStart) {
cleanStart = false
showDialog(this, SelectWalletDialog())
if (daemonModel.wallet == null) {
showDialog(this, SelectWalletDialog())
}
}
}

Expand All @@ -65,7 +84,10 @@ class MainActivity : AppCompatActivity() {
newFrag.title.observe(this, Observer { setTitle(it ?: "") })
newFrag.subtitle.observe(this, Observer { supportActionBar!!.setSubtitle(it) })
}
ft.commitNow()

// BottomNavigationView onClick is sometimes triggered after state has been saved
// (https://github.com/Electron-Cash/Electron-Cash/issues/1091).
ft.commitNowAllowingStateLoss()
}

private fun getFragment(id: Int): Fragment {
Expand All @@ -76,7 +98,7 @@ class MainActivity : AppCompatActivity() {
} else {
frag = FRAGMENTS[id]!!.java.newInstance()
supportFragmentManager.beginTransaction()
.add(flContent.id, frag, tag).commitNow()
.add(flContent.id, frag, tag).commitNowAllowingStateLoss()
return frag
}
}
Expand Down
29 changes: 14 additions & 15 deletions android/app/src/main/java/org/electroncash/electroncash3/Network.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ class NetworkFragment : Fragment(), MainFragment {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupVerticalList(rvIfaces)
daemonUpdate.observe(viewLifecycleOwner, Observer {
val ifaceItems = py.builtins.callAttr(
"sorted", daemonModel.network.get("interfaces")!!.callAttr("items"))
var status = getString(R.string.connected_to,
ifaceItems.callAttr("__len__").toJava(Int::class.java))

val isSplit = daemonModel.network.callAttr("get_blockchains")
.callAttr("__len__").toJava(Int::class.java) > 1
val ifaceLock = daemonModel.network.get("interface_lock")!!
ifaceLock.callAttr("acquire")
val ifaces = ArrayList(daemonModel.network.get("interfaces")!!.asMap().values)
ifaces.sortBy { it.get("server").toString() }
ifaceLock.callAttr("release")

var status = getString(R.string.connected_to, ifaces.size)
val isSplit = daemonModel.network.callAttr("get_blockchains").asMap().size > 1
if (isSplit) {
val curChain = daemonModel.network.callAttr("blockchain")
status += "\n" + getString(R.string.chain_split,
curChain.callAttr("get_base_height")
.toJava(Int::class.java))
curChain.callAttr("get_base_height").toInt())
}
tvStatus.text = status

Expand All @@ -74,23 +74,22 @@ class NetworkFragment : Fragment(), MainFragment {
} else {
tvServer.setText(R.string.not_connected)
}

rvIfaces.adapter = IfacesAdapter(activity!!, ifaceItems, isSplit)
rvIfaces.adapter = IfacesAdapter(activity!!, ifaces, isSplit)
})
}
}


class IfacesAdapter(val activity: FragmentActivity, val ifaceItems: PyObject, val isSplit: Boolean)
class IfacesAdapter(val activity: FragmentActivity, val ifaces: List<PyObject>,
val isSplit: Boolean)
: BoundAdapter<IfaceModel>(R.layout.iface) {

override fun getItemCount(): Int {
return ifaceItems.callAttr("__len__").toJava(Int::class.java)
return ifaces.size
}

override fun getItem(position: Int): IfaceModel {
val item = ifaceItems.callAttr("__getitem__", position)
return IfaceModel(item.callAttr("__getitem__", 1), isSplit)
return IfaceModel(ifaces.get(position), isSplit)
}

override fun onBindViewHolder(holder: BoundViewHolder<IfaceModel>, position: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ fun showQR(img: ImageView, text: String) {
// The layout already provides a margin of about 2 blocks, which is enough for all current
// scanners (https://qrworld.wordpress.com/2011/08/09/the-quiet-zone/).
val hints = mapOf(EncodeHintType.MARGIN to 0)

val resolution = app.resources.getDimensionPixelSize(R.dimen.qr_resolution)
val matrix = MultiFormatWriter().encode(
text, BarcodeFormat.QR_CODE, img.width, img.height, hints)
text, BarcodeFormat.QR_CODE, resolution, resolution, hints)
img.setImageBitmap(BarcodeEncoder().createBitmap(matrix))
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,16 @@ class RequestsFragment : Fragment(), MainFragment {
} else {
subtitle.value = null
rvRequests.adapter = RequestsAdapter(
activity!!, wallet.callAttr("get_sorted_requests", daemonModel.config))
activity!!,
wallet.callAttr("get_sorted_requests", daemonModel.config).asList())
btnAdd.show()
}
}
daemonUpdate.observe(viewLifecycleOwner, observer)
requestsUpdate.observe(viewLifecycleOwner, observer)

btnAdd.setOnClickListener {
if (daemonModel.wallet!!.callAttr("is_watching_only").toJava(Boolean::class.java)) {
if (daemonModel.wallet!!.callAttr("is_watching_only").toBoolean()) {
toast(R.string.this_wallet_is_watching_only_)
} else {
val address = wallet!!.callAttr("get_unused_address")
Expand All @@ -69,15 +70,15 @@ class RequestsFragment : Fragment(), MainFragment {
}


class RequestsAdapter(val activity: FragmentActivity, val requests: PyObject)
class RequestsAdapter(val activity: FragmentActivity, val requests: List<PyObject>)
: BoundAdapter<RequestModel>(R.layout.request_list) {

override fun getItemCount(): Int {
return requests.callAttr("__len__").toJava(Int::class.java)
return requests.size
}

override fun getItem(position: Int): RequestModel {
return RequestModel(requests.callAttr("__getitem__", position))
return RequestModel(requests.get(position))
}

override fun onBindViewHolder(holder: BoundViewHolder<RequestModel>, position: Int) {
Expand All @@ -90,10 +91,10 @@ class RequestsAdapter(val activity: FragmentActivity, val requests: PyObject)

class RequestModel(val request: PyObject) {
val address = getField("address").toString()
val amount = formatSatoshis(getField("amount").toJava(Long::class.java))
val amount = formatSatoshis(getField("amount").toLong())
val timestamp = libUtil.callAttr("format_time", getField("time")).toString()
val description = getField("memo").toString()
val status = formatStatus(getField("status").toJava(Int::class.java))
val status = formatStatus(getField("status").toInt())

private fun formatStatus(status: Int): Any {
return app.resources.getStringArray(R.array.payment_status)[status]
Expand Down

0 comments on commit aaed660

Please sign in to comment.