diff options
29 files changed, 321 insertions, 609 deletions
diff --git a/merchant-terminal/.gitlab-ci.yml b/merchant-terminal/.gitlab-ci.yml index 467824c..9bd889b 100644 --- a/merchant-terminal/.gitlab-ci.yml +++ b/merchant-terminal/.gitlab-ci.yml @@ -2,7 +2,7 @@ merchant_test: stage: test only: changes: - - "merchant-terminal" + - merchant-terminal/**/* script: ./gradlew :merchant-terminal:lint :merchant-terminal:assembleRelease artifacts: paths: @@ -15,7 +15,7 @@ merchant_deploy_nightly: refs: - master changes: - - "merchant-terminal" + - merchant-terminal/**/* needs: ["merchant_test"] script: # Ensure that key exists diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle index 594cab3..dd2527e 100644 --- a/merchant-terminal/build.gradle +++ b/merchant-terminal/build.gradle @@ -5,7 +5,9 @@ apply plugin: "androidx.navigation.safeargs.kotlin" android { compileSdkVersion 29 - buildToolsVersion "29.0.3" + //noinspection GradleDependency + buildToolsVersion "$build_tools_version" + defaultConfig { applicationId "net.taler.merchantpos" minSdkVersion 26 @@ -14,6 +16,7 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + buildTypes { release { minifyEnabled true @@ -43,9 +46,8 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' + implementation project(":taler-kotlin-common") + implementation 'com.google.android.material:material:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "androidx.recyclerview:recyclerview:1.1.0" @@ -55,17 +57,9 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - // ViewModel and LiveData - def lifecycle_version = "2.2.0" - implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" - // HTTP Requests implementation 'com.android.volley:volley:1.1.1' - // QR codes - implementation 'com.google.zxing:core:3.4.0' - implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" // JSON parsing and serialization diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt deleted file mode 100644 index 17ddd61..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/Amount.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.merchantpos - -import org.json.JSONObject - -data class Amount(val currency: String, val amount: String) { - @Suppress("unused") - fun isZero(): Boolean { - return amount.toDouble() == 0.0 - } - - companion object { - private const val FRACTIONAL_BASE = 1e8 - - @Suppress("unused") - fun fromJson(jsonAmount: JSONObject): Amount { - val amountCurrency = jsonAmount.getString("currency") - val amountValue = jsonAmount.getString("value") - val amountFraction = jsonAmount.getString("fraction") - val amountIntValue = Integer.parseInt(amountValue) - val amountIntFraction = Integer.parseInt(amountFraction) - return Amount( - amountCurrency, - (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString() - ) - } - - fun fromString(strAmount: String): Amount { - val components = strAmount.split(":") - return Amount(components[0], components[1]) - } - } -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt index 0c6bdfa..d6e3747 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/MainActivity.kt @@ -36,6 +36,7 @@ import androidx.navigation.ui.setupWithNavController import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.app_bar_main.* +import net.taler.common.NfcManager class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt deleted file mode 100644 index 09c1470..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/NfcManager.kt +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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.merchantpos - -import android.app.Activity -import android.content.Context -import android.nfc.NfcAdapter -import android.nfc.NfcAdapter.FLAG_READER_NFC_A -import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK -import android.nfc.Tag -import android.nfc.tech.IsoDep -import android.util.Log -import net.taler.merchantpos.Utils.hexStringToByteArray -import org.json.JSONObject -import java.io.ByteArrayOutputStream -import java.net.URL -import javax.net.ssl.HttpsURLConnection - -@Suppress("unused") -private const val TALER_AID = "A0000002471001" - -class NfcManager : NfcAdapter.ReaderCallback { - - companion object { - const val TAG = "taler-merchant" - - /** - * Returns true if NFC is supported and false otherwise. - */ - fun hasNfc(context: Context): Boolean { - return getNfcAdapter(context) != null - } - - /** - * Enables NFC reader mode. Don't forget to call [stop] afterwards. - */ - fun start(activity: Activity, nfcManager: NfcManager) { - getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager, nfcManager.flags, null) - } - - /** - * Disables NFC reader mode. Call after [start]. - */ - fun stop(activity: Activity) { - getNfcAdapter(activity)?.disableReaderMode(activity) - } - - private fun getNfcAdapter(context: Context): NfcAdapter? { - return NfcAdapter.getDefaultAdapter(context) - } - } - - private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK - - private var tagString: String? = null - private var currentTag: IsoDep? = null - - fun setTagString(tagString: String) { - this.tagString = tagString - } - - override fun onTagDiscovered(tag: Tag?) { - - Log.v(TAG, "tag discovered") - - val isoDep = IsoDep.get(tag) - isoDep.connect() - - currentTag = isoDep - - isoDep.transceive(apduSelectFile()) - - val tagString: String? = tagString - if (tagString != null) { - isoDep.transceive(apduPutTalerData(1, tagString.toByteArray())) - } - - // FIXME: use better pattern for sleeps in between requests - // -> start with fast polling, poll more slowly if no requests are coming - - while (true) { - try { - val reqFrame = isoDep.transceive(apduGetData()) - if (reqFrame.size < 2) { - Log.v(TAG, "request frame too small") - break - } - val req = ByteArray(reqFrame.size - 2) - if (req.isEmpty()) { - continue - } - reqFrame.copyInto(req, 0, 0, reqFrame.size - 2) - val jsonReq = JSONObject(req.toString(Charsets.UTF_8)) - val reqId = jsonReq.getInt("id") - Log.v(TAG, "got request $jsonReq") - val jsonInnerReq = jsonReq.getJSONObject("request") - val method = jsonInnerReq.getString("method") - val urlStr = jsonInnerReq.getString("url") - Log.v(TAG, "url '$urlStr'") - Log.v(TAG, "method '$method'") - val url = URL(urlStr) - val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection - conn.setRequestProperty("Accept", "application/json") - conn.connectTimeout = 5000 - conn.doInput = true - when (method) { - "get" -> { - conn.requestMethod = "GET" - } - "postJson" -> { - conn.requestMethod = "POST" - conn.doOutput = true - conn.setRequestProperty("Content-Type", "application/json; utf-8") - val body = jsonInnerReq.getString("body") - conn.outputStream.write(body.toByteArray(Charsets.UTF_8)) - } - else -> { - throw Exception("method not supported") - } - } - Log.v(TAG, "connecting") - conn.connect() - Log.v(TAG, "connected") - - val statusCode = conn.responseCode - val tunnelResp = JSONObject() - tunnelResp.put("id", reqId) - tunnelResp.put("status", conn.responseCode) - - if (statusCode == 200) { - val stream = conn.inputStream - val httpResp = stream.buffered().readBytes() - tunnelResp.put("responseJson", JSONObject(httpResp.toString(Charsets.UTF_8))) - } - - Log.v(TAG, "sending: $tunnelResp") - - isoDep.transceive(apduPutTalerData(2, tunnelResp.toString().toByteArray())) - } catch (e: Exception) { - Log.v(TAG, "exception during NFC loop: $e") - break - } - } - - isoDep.close() - } - - private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) { - when { - size == 0 -> { - // No size field needed! - } - size <= 255 -> // One byte size field - stream.write(size) - size <= 65535 -> { - stream.write(0) - // FIXME: is this supposed to be little or big endian? - stream.write(size and 0xFF) - stream.write((size ushr 8) and 0xFF) - } - else -> throw Error("payload too big") - } - } - - private fun apduSelectFile(): ByteArray { - return hexStringToByteArray("00A4040007A0000002471001") - } - - private fun apduPutData(payload: ByteArray): ByteArray { - val stream = ByteArrayOutputStream() - - // Class - stream.write(0x00) - - // Instruction 0xDA = put data - stream.write(0xDA) - - // Instruction parameters - // (proprietary encoding) - stream.write(0x01) - stream.write(0x00) - - writeApduLength(stream, payload.size) - - stream.write(payload) - - return stream.toByteArray() - } - - private fun apduPutTalerData(talerInst: Int, payload: ByteArray): ByteArray { - val realPayload = ByteArrayOutputStream() - realPayload.write(talerInst) - realPayload.write(payload) - return apduPutData(realPayload.toByteArray()) - } - - private fun apduGetData(): ByteArray { - val stream = ByteArrayOutputStream() - - // Class - stream.write(0x00) - - // Instruction 0xCA = get data - stream.write(0xCA) - - // Instruction parameters - // (proprietary encoding) - stream.write(0x01) - stream.write(0x00) - - // Max expected response size, two - // zero bytes denotes 65536 - stream.write(0x0) - stream.write(0x0) - - return stream.toByteArray() - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt deleted file mode 100644 index 595e7ac..0000000 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/QrCodeManager.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.merchantpos - -import android.graphics.Bitmap -import android.graphics.Bitmap.Config.RGB_565 -import android.graphics.Color.BLACK -import android.graphics.Color.WHITE -import com.google.zxing.BarcodeFormat.QR_CODE -import com.google.zxing.qrcode.QRCodeWriter - -object QrCodeManager { - - fun makeQrCode(text: String, size: Int = 256): Bitmap { - val qrCodeWriter = QRCodeWriter() - val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size) - val height = bitMatrix.height - val width = bitMatrix.width - val bmp = Bitmap.createBitmap(width, height, RGB_565) - for (x in 0 until width) { - for (y in 0 until height) { - bmp.setPixel(x, y, if (bitMatrix.get(x, y)) BLACK else WHITE) - } - } - return bmp - } - -} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt index a0c30d6..578debf 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/Utils.kt @@ -16,86 +16,12 @@ package net.taler.merchantpos -import android.content.Context -import android.text.format.DateUtils.DAY_IN_MILLIS -import android.text.format.DateUtils.FORMAT_ABBREV_MONTH -import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE -import android.text.format.DateUtils.FORMAT_NO_YEAR -import android.text.format.DateUtils.FORMAT_SHOW_DATE -import android.text.format.DateUtils.FORMAT_SHOW_TIME -import android.text.format.DateUtils.MINUTE_IN_MILLIS -import android.text.format.DateUtils.formatDateTime -import android.text.format.DateUtils.getRelativeTimeSpanString import android.view.View -import android.view.View.INVISIBLE -import android.view.View.VISIBLE import androidx.annotation.StringRes -import androidx.fragment.app.Fragment -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.Observer -import androidx.navigation.NavController -import androidx.navigation.NavDirections -import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.BaseTransientBottomBar.ANIMATION_MODE_FADE import com.google.android.material.snackbar.BaseTransientBottomBar.Duration import com.google.android.material.snackbar.Snackbar.make -object Utils { - - private const val HEX_CHARS = "0123456789ABCDEF" - - fun hexStringToByteArray(data: String): ByteArray { - val result = ByteArray(data.length / 2) - - for (i in data.indices step 2) { - val firstIndex = HEX_CHARS.indexOf(data[i]) - val secondIndex = HEX_CHARS.indexOf(data[i + 1]) - - val octet = firstIndex.shl(4).or(secondIndex) - result[i.shr(1)] = octet.toByte() - } - return result - } - - - private val HEX_CHARS_ARRAY = HEX_CHARS.toCharArray() - - @Suppress("unused") - fun toHex(byteArray: ByteArray): String { - val result = StringBuffer() - - byteArray.forEach { - val octet = it.toInt() - val firstIndex = (octet and 0xF0).ushr(4) - val secondIndex = octet and 0x0F - result.append(HEX_CHARS_ARRAY[firstIndex]) - result.append(HEX_CHARS_ARRAY[secondIndex]) - } - return result.toString() - } - -} - -fun View.fadeIn(endAction: () -> Unit = {}) { - if (visibility == VISIBLE) return - alpha = 0f - visibility = VISIBLE - animate().alpha(1f).withEndAction { - if (context != null) endAction.invoke() - }.start() -} - -fun View.fadeOut(endAction: () -> Unit = {}) { - if (visibility == INVISIBLE) return - animate().alpha(0f).withEndAction { - if (context == null) return@withEndAction - visibility = INVISIBLE - alpha = 1f - endAction.invoke() - }.start() -} - fun topSnackbar(view: View, text: CharSequence, @Duration duration: Int) { make(view, text, duration) .setAnimationMode(ANIMATION_MODE_FADE) @@ -106,50 +32,3 @@ fun topSnackbar(view: View, text: CharSequence, @Duration duration: Int) { fun topSnackbar(view: View, @StringRes resId: Int, @Duration duration: Int) { topSnackbar(view, view.resources.getText(resId), duration) } - -fun NavDirections.navigate(nav: NavController) = nav.navigate(this) - -fun Fragment.navigate(directions: NavDirections) = findNavController().navigate(directions) - -fun Long.toRelativeTime(context: Context): CharSequence { - val now = System.currentTimeMillis() - return if (now - this > DAY_IN_MILLIS * 2) { - val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR - formatDateTime(context, this, flags) - } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE) -} - -class CombinedLiveData<T, K, S>( - source1: LiveData<T>, - source2: LiveData<K>, - private val combine: (data1: T?, data2: K?) -> S -) : MediatorLiveData<S>() { - - private var data1: T? = null - private var data2: K? = null - - init { - super.addSource(source1) { t -> - data1 = t - value = combine(data1, data2) - } - super.addSource(source2) { k -> - data2 = k - value = combine(data1, data2) - } - } - - override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) { - throw UnsupportedOperationException() - } - - override fun <T : Any?> removeSource(toRemote: LiveData<T>) { - throw UnsupportedOperationException() - } -} - -/** - * Use this with 'when' expressions when you need it to handle all possibilities/branches. - */ -val <T> T.exhaustive: T - get() = this diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt index c370e33..c0c87dc 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFetcherFragment.kt @@ -23,14 +23,13 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToMerchantSettings import net.taler.merchantpos.config.ConfigFetcherFragmentDirections.Companion.actionConfigFetcherToOrder -import net.taler.merchantpos.navigate class ConfigFetcherFragment : Fragment() { @@ -52,7 +51,7 @@ class ConfigFetcherFragment : Fragment() { null -> return@Observer is ConfigUpdateResult.Error -> onNetworkError(result.msg) is ConfigUpdateResult.Success -> { - actionConfigFetcherToOrder().navigate(findNavController()) + navigate(actionConfigFetcherToOrder()) } } }) @@ -60,7 +59,7 @@ class ConfigFetcherFragment : Fragment() { private fun onNetworkError(msg: String) { Snackbar.make(view!!, msg, LENGTH_SHORT).show() - actionConfigFetcherToMerchantSettings().navigate(findNavController()) + navigate(actionConfigFetcherToMerchantSettings()) } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt index 2050e28..8141f0f 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfig.kt @@ -17,7 +17,13 @@ package net.taler.merchantpos.config import android.net.Uri +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonProperty +import net.taler.common.Amount +import net.taler.common.ContractProduct +import net.taler.common.Product +import net.taler.common.TalerUtils +import java.util.* data class Config( val configUrl: String, @@ -45,3 +51,42 @@ data class MerchantConfig( return uriBuilder.toString() } } + +data class Category( + val id: Int, + val name: String, + @JsonProperty("name_i18n") + val nameI18n: Map<String, String>? +) { + var selected: Boolean = false + val localizedName: String get() = TalerUtils.getLocalizedString(nameI18n, name) +} + +data class ConfigProduct( + @JsonIgnore + val id: String = UUID.randomUUID().toString(), + override val productId: String?, + override val description: String, + override val descriptionI18n: Map<String, String>?, + override val price: String, + override val location: String?, + override val image: String?, + val categories: List<Int>, + @JsonIgnore + val quantity: Int = 0 +) : Product() { + val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() } + + fun toContractProduct() = ContractProduct( + productId = productId, + description = description, + descriptionI18n = descriptionI18n, + price = price, + location = location, + image = image, + quantity = quantity + ) + + override fun equals(other: Any?) = other is ConfigProduct && id == other.id + override fun hashCode() = id.hashCode() +} diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt index aad1c93..a584af8 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/MerchantConfigFragment.kt @@ -28,14 +28,13 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_merchant_config.* +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R import net.taler.merchantpos.config.MerchantConfigFragmentDirections.Companion.actionSettingsToOrder -import net.taler.merchantpos.navigate import net.taler.merchantpos.topSnackbar /** @@ -149,7 +148,7 @@ class MerchantConfigFragment : Fragment() { onResultReceived() updateView() topSnackbar(view!!, getString(R.string.config_changed, currency), LENGTH_LONG) - actionSettingsToOrder().navigate(findNavController()) + navigate(actionSettingsToOrder()) } private fun onError(msg: String) { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt index faee226..fc3f93a 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt @@ -24,22 +24,15 @@ import com.android.volley.RequestQueue import com.android.volley.Response.ErrorListener import com.android.volley.Response.Listener import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import net.taler.merchantpos.Amount +import net.taler.common.Amount +import net.taler.common.Timestamp import net.taler.merchantpos.config.ConfigManager import net.taler.merchantpos.config.MerchantRequest import org.json.JSONObject -@JsonInclude(NON_EMPTY) -class Timestamp( - @JsonProperty("t_ms") - val ms: Long -) - data class HistoryItem( @JsonProperty("order_id") val orderId: String, diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt index 0c53f71..afa925d 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt @@ -35,14 +35,14 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT import kotlinx.android.synthetic.main.fragment_merchant_history.* +import net.taler.common.exhaustive +import net.taler.common.navigate +import net.taler.common.toRelativeTime import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.exhaustive import net.taler.merchantpos.history.HistoryItemAdapter.HistoryItemViewHolder import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionGlobalMerchantSettings import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionNavHistoryToRefundFragment -import net.taler.merchantpos.navigate -import net.taler.merchantpos.toRelativeTime import java.util.* private interface RefundClickListener { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt index 1797cea..aa2489a 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundFragment.kt @@ -28,15 +28,15 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_refund.* +import net.taler.common.fadeIn +import net.taler.common.fadeOut +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.fadeIn -import net.taler.merchantpos.fadeOut import net.taler.merchantpos.history.RefundFragmentDirections.Companion.actionRefundFragmentToRefundUriFragment import net.taler.merchantpos.history.RefundResult.Error import net.taler.merchantpos.history.RefundResult.PastDeadline import net.taler.merchantpos.history.RefundResult.Success -import net.taler.merchantpos.navigate class RefundFragment : Fragment() { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt index f2bd569..6e5b96d 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/history/RefundUriFragment.kt @@ -25,9 +25,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import kotlinx.android.synthetic.main.fragment_refund_uri.* +import net.taler.common.NfcManager.Companion.hasNfc +import net.taler.common.QrCodeManager.makeQrCode import net.taler.merchantpos.MainViewModel -import net.taler.merchantpos.NfcManager.Companion.hasNfc -import net.taler.merchantpos.QrCodeManager.makeQrCode import net.taler.merchantpos.R class RefundUriFragment : Fragment() { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt index 34b97c0..e935d4f 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt @@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView.Adapter import kotlinx.android.synthetic.main.fragment_categories.* import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R +import net.taler.merchantpos.config.Category import net.taler.merchantpos.order.CategoryAdapter.CategoryViewHolder interface CategorySelectionListener { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt index ff6061a..847326b 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/LiveOrder.kt @@ -20,7 +20,9 @@ import androidx.annotation.UiThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations -import net.taler.merchantpos.CombinedLiveData +import net.taler.common.CombinedLiveData +import net.taler.merchantpos.config.Category +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.RestartState.DISABLED import net.taler.merchantpos.order.RestartState.ENABLED import net.taler.merchantpos.order.RestartState.UNDO diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt index 63eda17..5954e63 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Definitions.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt @@ -16,105 +16,8 @@ package net.taler.merchantpos.order -import androidx.core.os.LocaleListCompat -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL -import com.fasterxml.jackson.annotation.JsonProperty -import net.taler.merchantpos.Amount -import java.util.* -import java.util.Locale.LanguageRange -import kotlin.collections.ArrayList -import kotlin.collections.HashMap - -data class Category( - val id: Int, - val name: String, - @JsonProperty("name_i18n") - val nameI18n: Map<String, String>? -) { - var selected: Boolean = false - val localizedName: String get() = getLocalizedString(nameI18n, name) -} - -@JsonInclude(NON_NULL) -abstract class Product { - @get:JsonProperty("product_id") - abstract val productId: String? - abstract val description: String - @get:JsonProperty("description_i18n") - abstract val descriptionI18n: Map<String, String>? - abstract val price: String - @get:JsonProperty("delivery_location") - abstract val location: String? - abstract val image: String? - @get:JsonIgnore - val localizedDescription: String - get() = getLocalizedString(descriptionI18n, description) -} - -data class ConfigProduct( - @JsonIgnore - val id: String = UUID.randomUUID().toString(), - override val productId: String?, - override val description: String, - override val descriptionI18n: Map<String, String>?, - override val price: String, - override val location: String?, - override val image: String?, - val categories: List<Int>, - @JsonIgnore - val quantity: Int = 0 -) : Product() { - val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() } - - override fun equals(other: Any?) = other is ConfigProduct && id == other.id - override fun hashCode() = id.hashCode() -} - -data class ContractProduct( - override val productId: String?, - override val description: String, - override val descriptionI18n: Map<String, String>?, - override val price: String, - override val location: String?, - override val image: String?, - val quantity: Int -) : Product() { - constructor(product: ConfigProduct) : this( - product.productId, - product.description, - product.descriptionI18n, - product.price, - product.location, - product.image, - product.quantity - ) -} - -private fun getLocalizedString(map: Map<String, String>?, default: String): String { - // just return the default, if it is the only element - if (map == null) return default - // create a priority list of language ranges from system locales - val locales = LocaleListCompat.getDefault() - val priorityList = ArrayList<LanguageRange>(locales.size()) - for (i in 0 until locales.size()) { - priorityList.add(LanguageRange(locales[i].toLanguageTag())) - } - // create a list of locales available in the given map - val availableLocales = map.keys.mapNotNull { - if (it == "_") return@mapNotNull null - val list = it.split("_") - when (list.size) { - 1 -> Locale(list[0]) - 2 -> Locale(list[0], list[1]) - 3 -> Locale(list[0], list[1], list[2]) - else -> null - } - } - val match = Locale.lookup(priorityList, availableLocales) - return match?.toString()?.let { map[it] } ?: default -} +import net.taler.merchantpos.config.Category +import net.taler.merchantpos.config.ConfigProduct data class Order(val id: Int, val availableCategories: Map<Int, Category>) { val products = ArrayList<ConfigProduct>() diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt index 49f7cf2..ad6cd87 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderFragment.kt @@ -23,12 +23,11 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer -import androidx.navigation.fragment.findNavController import androidx.transition.TransitionManager.beginDelayedTransition import kotlinx.android.synthetic.main.fragment_order.* +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.navigate import net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionGlobalConfigFetcher import net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToMerchantSettings import net.taler.merchantpos.order.OrderFragmentDirections.Companion.actionOrderToProcessPayment @@ -65,9 +64,9 @@ class OrderFragment : Fragment() { override fun onStart() { super.onStart() if (!viewModel.configManager.config.isValid()) { - actionOrderToMerchantSettings().navigate(findNavController()) + navigate(actionOrderToMerchantSettings()) } else if (viewModel.configManager.merchantConfig?.currency == null) { - actionGlobalConfigFetcher().navigate(findNavController()) + navigate(actionGlobalConfigFetcher()) } } @@ -108,7 +107,7 @@ class OrderFragment : Fragment() { completeButton.setOnClickListener { val order = liveOrder.order.value ?: return@setOnClickListener paymentManager.createPayment(order) - actionOrderToProcessPayment().navigate(findNavController()) + navigate(actionOrderToProcessPayment()) } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt index 48ddc57..a30c264 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderManager.kt @@ -24,8 +24,10 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations.map import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper -import net.taler.merchantpos.Amount.Companion.fromString +import net.taler.common.Amount.Companion.fromString import net.taler.merchantpos.R +import net.taler.merchantpos.config.Category +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.config.ConfigurationReceiver import net.taler.merchantpos.order.RestartState.ENABLED import org.json.JSONObject diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt index 1b70016..a90334b 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt @@ -37,10 +37,11 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlinx.android.synthetic.main.fragment_order_state.* +import net.taler.common.fadeIn +import net.taler.common.fadeOut import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R -import net.taler.merchantpos.fadeIn -import net.taler.merchantpos.fadeOut +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.OrderAdapter.OrderLineLookup import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt index 4704ad0..d4da73f 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt @@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlinx.android.synthetic.main.fragment_products.* import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R +import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.ProductAdapter.ProductViewHolder interface ProductSelectionListener { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt index 7f15816..4cfb069 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/PaymentManager.kt @@ -30,7 +30,6 @@ import com.android.volley.VolleyError import com.fasterxml.jackson.databind.ObjectMapper import net.taler.merchantpos.config.ConfigManager import net.taler.merchantpos.config.MerchantRequest -import net.taler.merchantpos.order.ContractProduct import net.taler.merchantpos.order.Order import org.json.JSONArray import org.json.JSONObject @@ -103,7 +102,7 @@ class PaymentManager( } private fun Order.getProductsJson(): JSONArray { - val contractProducts = products.map { ContractProduct(it) } + val contractProducts = products.map { it.toContractProduct() } val productsStr = mapper.writeValueAsString(contractProducts) return JSONArray(productsStr) } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt index 24f67f1..1d61894 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt @@ -27,13 +27,13 @@ import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import kotlinx.android.synthetic.main.fragment_process_payment.* +import net.taler.common.NfcManager.Companion.hasNfc +import net.taler.common.QrCodeManager.makeQrCode +import net.taler.common.fadeIn +import net.taler.common.fadeOut +import net.taler.common.navigate import net.taler.merchantpos.MainViewModel -import net.taler.merchantpos.NfcManager.Companion.hasNfc -import net.taler.merchantpos.QrCodeManager.makeQrCode import net.taler.merchantpos.R -import net.taler.merchantpos.fadeIn -import net.taler.merchantpos.fadeOut -import net.taler.merchantpos.navigate import net.taler.merchantpos.payment.ProcessPaymentFragmentDirections.Companion.actionProcessPaymentToPaymentSuccess import net.taler.merchantpos.topSnackbar @@ -69,7 +69,7 @@ class ProcessPaymentFragment : Fragment() { } if (payment.paid) { model.orderManager.onOrderPaid(payment.order.id) - actionProcessPaymentToPaymentSuccess().navigate(findNavController()) + navigate(actionProcessPaymentToPaymentSuccess()) return } payIntroView.fadeIn() diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle index d7c9362..1d45a54 100644 --- a/taler-kotlin-common/build.gradle +++ b/taler-kotlin-common/build.gradle @@ -47,10 +47,17 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' + // Navigation + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + // ViewModel and LiveData def lifecycle_version = "2.2.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // QR codes implementation 'com.google.zxing:core:3.4.0' // needs minSdkVersion 24+ + + // JSON parsing and serialization + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2" } diff --git a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt index 428ddef..0389db1 100644 --- a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt +++ b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt @@ -16,13 +16,14 @@ package net.taler.common +import org.json.JSONObject + data class Amount(val currency: String, val amount: String) { companion object { - + private const val FRACTIONAL_BASE = 1e8 private val SIGNED_REGEX = Regex("""([+\-])(\w+):([0-9.]+)""") - @Suppress("unused") fun fromString(strAmount: String): Amount { val components = strAmount.split(":") return Amount(components[0], components[1]) @@ -38,6 +39,22 @@ data class Amount(val currency: String, val amount: String) { // only display as many digits as required to precisely render the balance return Amount(currency, amountStr.removeSuffix(".0")) } + + fun fromJson(jsonAmount: JSONObject): Amount { + val amountCurrency = jsonAmount.getString("currency") + val amountValue = jsonAmount.getString("value") + val amountFraction = jsonAmount.getString("fraction") + val amountIntValue = Integer.parseInt(amountValue) + val amountIntFraction = Integer.parseInt(amountFraction) + return Amount( + amountCurrency, + (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString() + ) + } + } + + fun isZero(): Boolean { + return amount.toDouble() == 0.0 } override fun toString() = "$amount $currency" diff --git a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt index 2fafdf2..fc04da5 100644 --- a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt +++ b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt @@ -19,35 +19,65 @@ package net.taler.common import android.content.Context import android.content.Context.CONNECTIVITY_SERVICE import android.net.ConnectivityManager -import android.net.NetworkCapabilities -import android.os.Build +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.os.Build.VERSION.SDK_INT +import android.text.format.DateUtils +import android.text.format.DateUtils.DAY_IN_MILLIS +import android.text.format.DateUtils.FORMAT_ABBREV_MONTH +import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE +import android.text.format.DateUtils.FORMAT_NO_YEAR +import android.text.format.DateUtils.FORMAT_SHOW_DATE +import android.text.format.DateUtils.FORMAT_SHOW_TIME +import android.text.format.DateUtils.MINUTE_IN_MILLIS import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import androidx.fragment.app.Fragment +import androidx.navigation.NavDirections +import androidx.navigation.fragment.findNavController fun View.fadeIn(endAction: () -> Unit = {}) { - if (visibility == View.VISIBLE) return + if (visibility == VISIBLE) return alpha = 0f - visibility = View.VISIBLE + visibility = VISIBLE animate().alpha(1f).withEndAction { - endAction.invoke() + if (context != null) endAction.invoke() }.start() } fun View.fadeOut(endAction: () -> Unit = {}) { - if (visibility == View.INVISIBLE) return + if (visibility == INVISIBLE) return animate().alpha(0f).withEndAction { - visibility = View.INVISIBLE + if (context == null) return@withEndAction + visibility = INVISIBLE alpha = 1f endAction.invoke() }.start() } +/** + * Use this with 'when' expressions when you need it to handle all possibilities/branches. + */ +val <T> T.exhaustive: T + get() = this + fun Context.isOnline(): Boolean { val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager - return if (Build.VERSION.SDK_INT < 29) { + return if (SDK_INT < 29) { @Suppress("DEPRECATION") cm.activeNetworkInfo?.isConnected == true } else { val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + capabilities.hasCapability(NET_CAPABILITY_INTERNET) } } + +fun Fragment.navigate(directions: NavDirections) = findNavController().navigate(directions) + +fun Long.toRelativeTime(context: Context): CharSequence { + val now = System.currentTimeMillis() + return if (now - this > DAY_IN_MILLIS * 2) { + val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR + DateUtils.formatDateTime(context, this, flags) + } else DateUtils.getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE) +} diff --git a/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt b/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt new file mode 100644 index 0000000..4e7016b --- /dev/null +++ b/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt @@ -0,0 +1,51 @@ +/* + * 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.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer + +class CombinedLiveData<T, K, S>( + source1: LiveData<T>, + source2: LiveData<K>, + private val combine: (data1: T?, data2: K?) -> S +) : MediatorLiveData<S>() { + + private var data1: T? = null + private var data2: K? = null + + init { + super.addSource(source1) { t -> + data1 = t + value = combine(data1, data2) + } + super.addSource(source2) { k -> + data2 = k + value = combine(data1, data2) + } + } + + override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) { + throw UnsupportedOperationException() + } + + override fun <T : Any?> removeSource(toRemote: LiveData<T>) { + throw UnsupportedOperationException() + } + +} diff --git a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt new file mode 100644 index 0000000..1e70b6f --- /dev/null +++ b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt @@ -0,0 +1,61 @@ +/* + * 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.common + +import androidx.annotation.RequiresApi +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.annotation.JsonProperty +import net.taler.common.TalerUtils.getLocalizedString + +@JsonInclude(NON_NULL) +abstract class Product { + @get:JsonProperty("product_id") + abstract val productId: String? + abstract val description: String + + @get:JsonProperty("description_i18n") + abstract val descriptionI18n: Map<String, String>? + abstract val price: String + + @get:JsonProperty("delivery_location") + abstract val location: String? + abstract val image: String? + + @get:JsonIgnore + val localizedDescription: String + @RequiresApi(26) + get() = getLocalizedString(descriptionI18n, description) +} + +data class ContractProduct( + override val productId: String?, + override val description: String, + override val descriptionI18n: Map<String, String>?, + override val price: String, + override val location: String?, + override val image: String?, + val quantity: Int +) : Product() + +@JsonInclude(NON_EMPTY) +class Timestamp( + @JsonProperty("t_ms") + val ms: Long +) diff --git a/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt new file mode 100644 index 0000000..cb1622e --- /dev/null +++ b/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt @@ -0,0 +1,51 @@ +/* + * 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.common + +import androidx.annotation.RequiresApi +import androidx.core.os.LocaleListCompat +import java.util.* +import kotlin.collections.ArrayList + +object TalerUtils { + + @RequiresApi(26) + fun getLocalizedString(map: Map<String, String>?, default: String): String { + // just return the default, if it is the only element + if (map == null) return default + // create a priority list of language ranges from system locales + val locales = LocaleListCompat.getDefault() + val priorityList = ArrayList<Locale.LanguageRange>(locales.size()) + for (i in 0 until locales.size()) { + priorityList.add(Locale.LanguageRange(locales[i].toLanguageTag())) + } + // create a list of locales available in the given map + val availableLocales = map.keys.mapNotNull { + if (it == "_") return@mapNotNull null + val list = it.split("_") + when (list.size) { + 1 -> Locale(list[0]) + 2 -> Locale(list[0], list[1]) + 3 -> Locale(list[0], list[1], list[2]) + else -> null + } + } + val match = Locale.lookup(priorityList, availableLocales) + return match?.toString()?.let { map[it] } ?: default + } + +} |