diff options
64 files changed, 1078 insertions, 161 deletions
diff --git a/.gitmodules b/.gitmodules index 29c8ad2..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "wallet-kotlin"] - path = multiplatform - url = git://git.taler.net/wallet-kotlin.git diff --git a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/AuthenticationFragment.kt b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/AuthenticationFragment.kt index 59d0410..da947b0 100644 --- a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/AuthenticationFragment.kt +++ b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/AuthenticationFragment.kt @@ -29,10 +29,10 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import com.google.android.material.card.MaterialCardView -import kotlinx.android.synthetic.main.fragment_authentication.* -import net.taler.lib.common.Amount +import net.taler.common.Amount import org.gnu.anastasis.ui.MainViewModel import org.gnu.anastasis.ui.R +import org.gnu.anastasis.ui.databinding.FragmentAuthenticationBinding class AuthenticationFragment : Fragment() { @@ -40,6 +40,12 @@ class AuthenticationFragment : Fragment() { private var price: Amount = Amount.zero("KUDOS") + private var _binding: FragmentAuthenticationBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -50,46 +56,46 @@ class AuthenticationFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - passwordCard.setOnClickListener { + binding.passwordCard.setOnClickListener { showDialog( R.id.action_nav_anastasis_authentication_to_securityQuestionFragment, - passwordCard, + binding.passwordCard, "question_card" ) } - postidentCard.setOnClickListener { + binding.postidentCard.setOnClickListener { toggleCard( - postidentCard, + binding.postidentCard, Amount.fromJSONString("KUDOS:3.5") ) } - smsCard.setOnClickListener { + binding.smsCard.setOnClickListener { showDialog( R.id.action_nav_anastasis_authentication_to_smsFragment, - smsCard, + binding.smsCard, "sms_card" ) } - videoCard.setOnClickListener { + binding.videoCard.setOnClickListener { showDialog( R.id.action_nav_anastasis_authentication_to_videoFragment, - videoCard, + binding.videoCard, "video_card" ) } viewModel.securityQuestionChecked.observe(viewLifecycleOwner, { checked -> - passwordCard.isChecked = checked + binding.passwordCard.isChecked = checked updatePrice(checked, Amount.fromJSONString("KUDOS:0.5")) updateNextButtonState() }) viewModel.smsChecked.observe(viewLifecycleOwner, { checked -> - smsCard.isChecked = checked + binding.smsCard.isChecked = checked updatePrice(checked, Amount.fromJSONString("KUDOS:1.0")) updateNextButtonState() }) viewModel.videoChecked.observe(viewLifecycleOwner, { checked -> - videoCard.isChecked = checked + binding.videoCard.isChecked = checked updatePrice(checked, Amount.fromJSONString("KUDOS:2.25")) updateNextButtonState() }) @@ -113,16 +119,16 @@ class AuthenticationFragment : Fragment() { private fun updatePrice(add: Boolean, amount: Amount) { if (add) price += amount else price -= amount - recoveryCostView.text = "Recovery cost: $price" + binding.recoveryCostView.text = "Recovery cost: $price" } private fun updateNextButtonState() { var numChecked = 0 - numChecked += if (passwordCard.isChecked) 1 else 0 - numChecked += if (postidentCard.isChecked) 1 else 0 - numChecked += if (smsCard.isChecked) 1 else 0 - numChecked += if (videoCard.isChecked) 1 else 0 - nextAuthButton.isEnabled = numChecked >= 2 + numChecked += if (binding.passwordCard.isChecked) 1 else 0 + numChecked += if (binding.postidentCard.isChecked) 1 else 0 + numChecked += if (binding.smsCard.isChecked) 1 else 0 + numChecked += if (binding.videoCard.isChecked) 1 else 0 + binding.nextAuthButton.isEnabled = numChecked >= 2 } } diff --git a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SecurityQuestionFragment.kt b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SecurityQuestionFragment.kt index 7353174..0796610 100644 --- a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SecurityQuestionFragment.kt +++ b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SecurityQuestionFragment.kt @@ -23,30 +23,35 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController -import com.google.android.material.transition.MaterialContainerTransform -import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS -import kotlinx.android.synthetic.main.fragment_security_question.* import org.gnu.anastasis.ui.MainViewModel -import org.gnu.anastasis.ui.R +import org.gnu.anastasis.ui.databinding.FragmentSecurityQuestionBinding class SecurityQuestionFragment : Fragment() { private val viewModel: MainViewModel by activityViewModels() + private var _binding: FragmentSecurityQuestionBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - sharedElementEnterTransition = MaterialContainerTransform().apply { - fadeMode = FADE_MODE_CROSS - } - return inflater.inflate(R.layout.fragment_security_question, container, false).apply { - transitionName = "question_card" - } + _binding = FragmentSecurityQuestionBinding.inflate(inflater, container, false) + val view = binding.root + return view + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - saveQuestionButton.setOnClickListener { + binding.saveQuestionButton.setOnClickListener { viewModel.securityQuestionChecked.value = true findNavController().popBackStack() } diff --git a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SmsFragment.kt b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SmsFragment.kt index a5d872d..413f472 100644 --- a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SmsFragment.kt +++ b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/SmsFragment.kt @@ -32,10 +32,11 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.transition.MaterialContainerTransform import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS -import kotlinx.android.synthetic.main.fragment_sms.* import org.gnu.anastasis.ui.MainViewModel import org.gnu.anastasis.ui.PERMISSION_REQUEST_CODE import org.gnu.anastasis.ui.R +import org.gnu.anastasis.ui.databinding.FragmentSecurityQuestionBinding +import org.gnu.anastasis.ui.databinding.FragmentSmsBinding private const val PERMISSION = Manifest.permission.READ_PHONE_STATE @@ -43,10 +44,17 @@ class SmsFragment : Fragment() { private val viewModel: MainViewModel by activityViewModels() + private var _binding: FragmentSmsBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + _binding = FragmentSmsBinding.inflate(inflater, container, false) sharedElementEnterTransition = MaterialContainerTransform().apply { fadeMode = FADE_MODE_CROSS } @@ -55,11 +63,16 @@ class SmsFragment : Fragment() { } } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - smsView.editText?.setOnFocusChangeListener { _, hasFocus -> + binding.smsView.editText?.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) checkPerm() } - saveSmsButton.setOnClickListener { + binding.saveSmsButton.setOnClickListener { viewModel.smsChecked.value = true findNavController().popBackStack() } @@ -98,8 +111,8 @@ class SmsFragment : Fragment() { private fun fillPhoneNumber() { val telephonyService = requireContext().getSystemService<TelephonyManager>() telephonyService?.line1Number?.let { phoneNumber -> - smsView.editText?.setText(phoneNumber) - smsView.editText?.setSelection(phoneNumber.length) + binding.smsView.editText?.setText(phoneNumber) + binding.smsView.editText?.setSelection(phoneNumber.length) } } diff --git a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/VideoFragment.kt b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/VideoFragment.kt index 6cd80ce..6716f8a 100644 --- a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/VideoFragment.kt +++ b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/authentication/VideoFragment.kt @@ -35,9 +35,9 @@ import androidx.navigation.fragment.findNavController import androidx.transition.TransitionManager.beginDelayedTransition import com.google.android.material.transition.MaterialContainerTransform import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS -import kotlinx.android.synthetic.main.fragment_video.* import org.gnu.anastasis.ui.MainViewModel import org.gnu.anastasis.ui.R +import org.gnu.anastasis.ui.databinding.FragmentVideoBinding import java.io.FileDescriptor private const val REQUEST_IMAGE_CAPTURE = 1 @@ -47,10 +47,17 @@ class VideoFragment : Fragment() { private val viewModel: MainViewModel by activityViewModels() + private var _binding: FragmentVideoBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + _binding = FragmentVideoBinding.inflate(inflater, container, false) sharedElementEnterTransition = MaterialContainerTransform().apply { fadeMode = FADE_MODE_CROSS } @@ -59,8 +66,13 @@ class VideoFragment : Fragment() { } } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - takePhotoButton.setOnClickListener { + binding.takePhotoButton.setOnClickListener { val pm = requireContext().packageManager Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent -> takePictureIntent.resolveActivity(pm)?.also { @@ -70,7 +82,7 @@ class VideoFragment : Fragment() { } } } - choosePhotoButton.setOnClickListener { + binding.choosePhotoButton.setOnClickListener { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "image/*" @@ -80,7 +92,7 @@ class VideoFragment : Fragment() { ) } - saveVideoButton.setOnClickListener { + binding.saveVideoButton.setOnClickListener { viewModel.videoChecked.value = true findNavController().popBackStack() } @@ -99,12 +111,14 @@ class VideoFragment : Fragment() { } private fun showImage(bitmap: Bitmap) { - photoView.setImageBitmap(bitmap) - beginDelayedTransition(view as ViewGroup) - photoView.visibility = VISIBLE - takePhotoButton.visibility = GONE - choosePhotoButton.visibility = GONE - saveVideoButton.isEnabled = true + with (binding) { + photoView.setImageBitmap(bitmap) + beginDelayedTransition(view as ViewGroup) + photoView.visibility = VISIBLE + takePhotoButton.visibility = GONE + choosePhotoButton.visibility = GONE + saveVideoButton.isEnabled = true + } } private fun getBitmapFromUri(uri: Uri): Bitmap { diff --git a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/identity/ChangeLocationFragment.kt b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/identity/ChangeLocationFragment.kt index 5b68d36..00eec11 100644 --- a/anastasis-ui/src/main/java/org/gnu/anastasis/ui/identity/ChangeLocationFragment.kt +++ b/anastasis-ui/src/main/java/org/gnu/anastasis/ui/identity/ChangeLocationFragment.kt @@ -23,14 +23,20 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController -import kotlinx.android.synthetic.main.fragment_change_location.* import org.gnu.anastasis.ui.MainViewModel import org.gnu.anastasis.ui.R +import org.gnu.anastasis.ui.databinding.FragmentChangeLocationBinding class ChangeLocationFragment : Fragment() { private val viewModel: MainViewModel by activityViewModels() + private var _binding: FragmentChangeLocationBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -40,16 +46,16 @@ class ChangeLocationFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - switzerlandView.setOnClickListener { + binding.switzerlandView.setOnClickListener { changeCountry(LOCATIONS[0]) } - germanyView.setOnClickListener { + binding.germanyView.setOnClickListener { changeCountry(LOCATIONS[1]) } - usaView.setOnClickListener { + binding.usaView.setOnClickListener { changeCountry(LOCATIONS[2]) } - indiaView.setOnClickListener { + binding.indiaView.setOnClickListener { changeCountry(LOCATIONS[3]) } } diff --git a/build.gradle b/build.gradle index b274284..20b5c78 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ buildscript { + ext.kotlin_version = '1.6.10' ext.ktor_version = '2.0.1' ext.nav_version = '2.4.2' ext.material_version = '1.6.0' diff --git a/cashier/build.gradle b/cashier/build.gradle index 1c9c2b6..ea59f23 100644 --- a/cashier/build.gradle +++ b/cashier/build.gradle @@ -85,6 +85,9 @@ dependencies { implementation "io.ktor:ktor-client:$ktor_version" implementation "io.ktor:ktor-client-okhttp:$ktor_version" implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version" + implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" + implementation "io.ktor:ktor-client-content-negotiation:$ktor_version" + implementation "io.ktor:ktor-server-call-logging:$ktor_version" testImplementation "junit:junit:$junit_version" diff --git a/cashier/src/main/java/net/taler/cashier/AboutDialogFragment.kt b/cashier/src/main/java/net/taler/cashier/AboutDialogFragment.kt index cdea792..3da49d2 100644 --- a/cashier/src/main/java/net/taler/cashier/AboutDialogFragment.kt +++ b/cashier/src/main/java/net/taler/cashier/AboutDialogFragment.kt @@ -30,7 +30,7 @@ import net.taler.cashier.BuildConfig.VERSION_NAME import net.taler.cashier.config.VERSION_BANK import net.taler.cashier.databinding.FragmentAboutDialogBinding import net.taler.cashier.databinding.FragmentBalanceBinding -import net.taler.lib.common.Version +import net.taler.common.Version class AboutDialogFragment : DialogFragment() { diff --git a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt index 002301c..fa9600b 100644 --- a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt +++ b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt @@ -32,10 +32,10 @@ import net.taler.cashier.BalanceFragmentDirections.Companion.actionBalanceFragme import net.taler.cashier.databinding.FragmentBalanceBinding import net.taler.cashier.withdraw.LastTransaction import net.taler.cashier.withdraw.WithdrawStatus +import net.taler.common.Amount import net.taler.common.exhaustive import net.taler.common.fadeIn import net.taler.common.fadeOut -import net.taler.lib.common.Amount sealed class BalanceResult { class Error(val msg: String) : BalanceResult() diff --git a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt index 253c7d5..1c819b9 100644 --- a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt +++ b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt @@ -24,17 +24,16 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.features.json.JsonFeature -import io.ktor.client.features.json.serializer.KotlinxSerializer +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json import net.taler.cashier.HttpHelper.makeJsonGetRequest import net.taler.cashier.config.ConfigManager import net.taler.cashier.withdraw.WithdrawManager +import net.taler.common.Amount +import net.taler.common.AmountParserException import net.taler.common.isOnline -import net.taler.lib.common.Amount -import net.taler.lib.common.AmountParserException private val TAG = MainViewModel::class.java.simpleName @@ -46,12 +45,8 @@ class MainViewModel(private val app: Application) : AndroidViewModel(app) { retryOnConnectionFailure(true) } } - install(JsonFeature) { - serializer = KotlinxSerializer( - Json { - ignoreUnknownKeys = true - } - ) + install(ContentNegotiation) { + json() } } val configManager = ConfigManager(app, viewModelScope, httpClient) diff --git a/cashier/src/main/java/net/taler/cashier/Response.kt b/cashier/src/main/java/net/taler/cashier/Response.kt index 6a72604..89b7b33 100644 --- a/cashier/src/main/java/net/taler/cashier/Response.kt +++ b/cashier/src/main/java/net/taler/cashier/Response.kt @@ -18,8 +18,9 @@ package net.taler.cashier import android.content.Context import android.util.Log +import io.ktor.client.call.body import io.ktor.client.call.receive -import io.ktor.client.features.ResponseException +import io.ktor.client.plugins.ResponseException import io.ktor.http.HttpStatusCode import kotlinx.serialization.Serializable import net.taler.common.isOnline @@ -47,7 +48,7 @@ class Response<out T> private constructor( val response = e.response return try { Log.e("TEST", "TRY RECEIVE $response") - val error: Error = response.receive() + val error: Error = response.body() "Error ${error.code}: ${error.hint}" } catch (ex: Exception) { "Status code: ${response.status.value}" diff --git a/cashier/src/main/java/net/taler/cashier/SignedAmount.kt b/cashier/src/main/java/net/taler/cashier/SignedAmount.kt index e79acfd..4f624ae 100644 --- a/cashier/src/main/java/net/taler/cashier/SignedAmount.kt +++ b/cashier/src/main/java/net/taler/cashier/SignedAmount.kt @@ -16,7 +16,7 @@ package net.taler.cashier -import net.taler.lib.common.Amount +import net.taler.common.Amount data class SignedAmount( val positive: Boolean, diff --git a/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt index 0718963..882348f 100644 --- a/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt +++ b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt @@ -39,8 +39,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.taler.cashier.Response import net.taler.cashier.Response.Companion.response +import net.taler.common.Version import net.taler.common.getIncompatibleStringOrNull -import net.taler.lib.common.Version val VERSION_BANK = Version(0, 0, 0) private const val PREF_NAME = "net.taler.cashier.prefs" @@ -126,7 +126,7 @@ class ConfigManager( val balanceResponse = response { val authUrl = "${config.bankUrl}/accounts/${config.username}" Log.d(TAG, "Checking auth: $authUrl") - httpClient.get<Unit>(authUrl) { + httpClient.get(authUrl) { header(Authorization, config.basicAuth) } } diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt index 5d34bba..360bded 100644 --- a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt +++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt @@ -34,9 +34,9 @@ import net.taler.cashier.HttpJsonResult.Error import net.taler.cashier.HttpJsonResult.Success import net.taler.cashier.MainViewModel import net.taler.cashier.R +import net.taler.common.Amount import net.taler.common.QrCodeManager.makeQrCode import net.taler.common.isOnline -import net.taler.lib.common.Amount import org.json.JSONObject import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS diff --git a/merchant-lib/build.gradle b/merchant-lib/build.gradle index c554b77..c31009d 100644 --- a/merchant-lib/build.gradle +++ b/merchant-lib/build.gradle @@ -57,9 +57,11 @@ dependencies { api "io.ktor:ktor-client:$ktor_version" api "io.ktor:ktor-client-okhttp:$ktor_version" api "io.ktor:ktor-client-serialization-jvm:$ktor_version" + api "io.ktor:ktor-client-content-negotiation:$ktor_version" + implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" + implementation "io.ktor:ktor-server-call-logging:$ktor_version" testImplementation "junit:junit:$junit_version" testImplementation "io.ktor:ktor-client-mock-jvm:$ktor_version" - testImplementation "io.ktor:ktor-client-logging-jvm:$ktor_version" testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1' } diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt index 0d22f91..d973813 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt @@ -17,21 +17,22 @@ package net.taler.merchantlib import io.ktor.client.HttpClient +import io.ktor.client.call.body import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.features.json.JsonFeature -import io.ktor.client.features.json.serializer.KotlinxSerializer +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.delete import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.post +import io.ktor.client.request.setBody import io.ktor.http.ContentType.Application.Json import io.ktor.http.HttpHeaders.Authorization import io.ktor.http.contentType import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import net.taler.merchantlib.Response.Companion.response +import io.ktor.serialization.kotlinx.json.* class MerchantApi( private val httpClient: HttpClient = getDefaultHttpClient(), @@ -52,8 +53,8 @@ class MerchantApi( httpClient.post(merchantConfig.urlFor("private/orders")) { header(Authorization, "ApiKey ${merchantConfig.apiKey}") contentType(Json) - body = orderRequest - } as PostOrderResponse + setBody(orderRequest) + }.body() } } @@ -64,7 +65,7 @@ class MerchantApi( response { httpClient.get(merchantConfig.urlFor("private/orders/$orderId")) { header(Authorization, "ApiKey ${merchantConfig.apiKey}") - } as CheckPaymentResponse + }.body() } } @@ -97,7 +98,7 @@ class MerchantApi( httpClient.post(merchantConfig.urlFor("private/orders/$orderId/refund")) { header(Authorization, "ApiKey ${merchantConfig.apiKey}") contentType(Json) - body = request + setBody(request) } as RefundResponse } } @@ -109,15 +110,7 @@ fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) { retryOnConnectionFailure(true) } } - install(JsonFeature) { - serializer = getSerializer() + install(ContentNegotiation) { + json() } } - -fun getSerializer() = KotlinxSerializer( - Json { - encodeDefaults = false - ignoreUnknownKeys = true - classDiscriminator = "order_status" - } -) diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt b/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt index dfd989b..b1ff5b1 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/OrderHistory.kt @@ -18,8 +18,8 @@ package net.taler.merchantlib import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import net.taler.lib.common.Amount -import net.taler.lib.common.Timestamp +import net.taler.common.Amount +import net.taler.common.Timestamp @Serializable data class OrderHistory( diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt index 166ca3c..9572e07 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Orders.kt @@ -19,8 +19,8 @@ package net.taler.merchantlib import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.taler.common.ContractTerms +import net.taler.common.Duration import net.taler.lib.android.CustomClassDiscriminator -import net.taler.lib.common.Duration @Serializable data class PostOrderRequest( diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt index b78b571..58073fa 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Refunds.kt @@ -18,8 +18,7 @@ package net.taler.merchantlib import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import net.taler.lib.common.Amount - +import net.taler.common.Amount @Serializable data class RefundRequest( /** diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt index 6fd0e37..b7ba1ac 100644 --- a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt +++ b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt @@ -16,10 +16,10 @@ package net.taler.merchantlib -import io.ktor.client.call.receive -import io.ktor.client.features.ClientRequestException -import io.ktor.client.features.ResponseException -import io.ktor.client.features.ServerResponseException +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.ResponseException +import io.ktor.client.plugins.ServerResponseException import kotlinx.serialization.Serializable class Response<out T> private constructor( @@ -72,7 +72,7 @@ class Response<out T> private constructor( private suspend fun getExceptionString(e: ResponseException): String { val response = e.response return try { - val error: Error = response.receive() + val error: Error = response.body() "Error ${error.code}: ${error.hint}" } catch (ex: Exception) { "Status code: ${response.status.value}" diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt index 6abacfd..8a45c9f 100644 --- a/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt +++ b/merchant-lib/src/test/java/net/taler/merchantlib/MerchantApiTest.kt @@ -20,10 +20,10 @@ import io.ktor.http.HttpStatusCode.Companion.NotFound import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineDispatcher +import net.taler.common.Amount import net.taler.common.ContractProduct import net.taler.common.ContractTerms -import net.taler.lib.common.Amount -import net.taler.lib.common.Timestamp +import net.taler.common.Timestamp import net.taler.merchantlib.MockHttpClient.giveJsonResponse import net.taler.merchantlib.MockHttpClient.httpClient import org.junit.Assert.assertEquals diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt b/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt index c8e6f22..880228c 100644 --- a/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt +++ b/merchant-lib/src/test/java/net/taler/merchantlib/MockHttpClient.kt @@ -20,11 +20,6 @@ import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockEngineConfig import io.ktor.client.engine.mock.respond -import io.ktor.client.features.json.JsonFeature -import io.ktor.client.features.logging.LogLevel -import io.ktor.client.features.logging.Logger -import io.ktor.client.features.logging.Logging -import io.ktor.client.features.logging.SIMPLE import io.ktor.http.ContentType.Application.Json import io.ktor.http.HttpStatusCode import io.ktor.http.Url @@ -38,13 +33,6 @@ import org.junit.Assert.assertEquals object MockHttpClient { val httpClient = HttpClient(MockEngine) { - install(JsonFeature) { - serializer = getSerializer() - } - install(Logging) { - logger = Logger.SIMPLE - level = LogLevel.ALL - } engine { addHandler { error("No response handler set") } } diff --git a/merchant-terminal/src/main/AndroidManifest.xml b/merchant-terminal/src/main/AndroidManifest.xml index eb7940f..f7b4929 100644 --- a/merchant-terminal/src/main/AndroidManifest.xml +++ b/merchant-terminal/src/main/AndroidManifest.xml @@ -39,6 +39,7 @@ <activity android:name=".MainActivity" android:label="@string/app_name" + android:exported="true" android:screenOrientation="landscape" android:theme="@style/AppTheme.NoActionBar" tools:ignore="LockedOrientationActivity"> diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt index 165bb8e..4327f4e 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt @@ -26,7 +26,8 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import io.ktor.client.HttpClient -import io.ktor.client.features.ClientRequestException +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.http.HttpHeaders.Authorization @@ -34,8 +35,8 @@ import io.ktor.http.HttpStatusCode.Companion.Unauthorized import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import net.taler.common.Version import net.taler.common.getIncompatibleStringOrNull -import net.taler.lib.common.Version import net.taler.merchantlib.ConfigResponse import net.taler.merchantlib.MerchantApi import net.taler.merchantlib.MerchantConfig @@ -105,7 +106,7 @@ class ConfigManager( val credentials = "${config.username}:${config.password}" val auth = ("Basic ${encodeToString(credentials.toByteArray(), NO_WRAP)}") header(Authorization, auth) - } + }.body() val merchantConfig = posConfig.merchantConfig // get config from merchant backend API api.getConfig(merchantConfig.baseUrl).handleSuspend(::onNetworkError) { diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt index 971f92c..1f1ab74 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/config/PosConfig.kt @@ -19,10 +19,10 @@ package net.taler.merchantpos.config import android.os.Build.VERSION.SDK_INT import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.taler.common.Amount import net.taler.common.ContractProduct import net.taler.common.Product import net.taler.common.TalerUtils -import net.taler.lib.common.Amount import net.taler.merchantlib.MerchantConfig import java.util.UUID 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 f48c1db..ad9af74 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,8 +20,8 @@ import androidx.annotation.UiThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations +import net.taler.common.Amount import net.taler.common.CombinedLiveData -import net.taler.lib.common.Amount import net.taler.merchantpos.config.Category import net.taler.merchantpos.config.ConfigProduct import net.taler.merchantpos.order.RestartState.DISABLED diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt index 0bea20c..9860dbd 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/order/Order.kt @@ -16,10 +16,10 @@ package net.taler.merchantpos.order +import net.taler.common.Amount import net.taler.common.ContractTerms +import net.taler.common.Timestamp import net.taler.common.now -import net.taler.lib.common.Amount -import net.taler.lib.common.Timestamp import net.taler.merchantpos.config.Category import net.taler.merchantpos.config.ConfigProduct import java.net.URLEncoder 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 d86f504..c4a5228 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 @@ -26,6 +26,7 @@ import androidx.recyclerview.selection.SelectionPredicates import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.LinearLayoutManager +import net.taler.common.Amount import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.merchantpos.MainViewModel @@ -82,11 +83,11 @@ class OrderStateFragment : Fragment() { liveOrder.selectOrderLine(item) } }) - liveOrder.order.observe(viewLifecycleOwner, { order -> + liveOrder.order.observe(viewLifecycleOwner) { order -> if (order == null) return@observe onOrderChanged(order, tracker) - }) - liveOrder.orderTotal.observe(viewLifecycleOwner, { orderTotal -> + } + liveOrder.orderTotal.observe(viewLifecycleOwner) { orderTotal: Amount -> if (orderTotal.isZero()) { ui.totalView.fadeOut() ui.totalView.text = null @@ -94,7 +95,7 @@ class OrderStateFragment : Fragment() { ui.totalView.text = getString(R.string.order_total, orderTotal) ui.totalView.fadeIn() } - }) + } } override fun onSaveInstanceState(outState: Bundle) { 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 98161db..d703a31 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 @@ -26,8 +26,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import net.taler.common.Duration import net.taler.common.assertUiThread -import net.taler.lib.common.Duration import net.taler.merchantlib.CheckPaymentResponse import net.taler.merchantlib.MerchantApi import net.taler.merchantlib.PostOrderRequest diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt index bb98dbd..03c786f 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundFragment.kt @@ -24,12 +24,12 @@ import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import net.taler.common.Amount +import net.taler.common.AmountParserException import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.common.navigate import net.taler.common.showError -import net.taler.lib.common.Amount -import net.taler.lib.common.AmountParserException import net.taler.merchantlib.OrderHistoryEntry import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt index 8b3efca..849dbaa 100644 --- a/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt +++ b/merchant-terminal/src/main/java/net/taler/merchantpos/refund/RefundManager.kt @@ -21,8 +21,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import net.taler.common.Amount import net.taler.common.assertUiThread -import net.taler.lib.common.Amount import net.taler.merchantlib.MerchantApi import net.taler.merchantlib.OrderHistoryEntry import net.taler.merchantlib.RefundRequest diff --git a/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt b/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt index ca48b6e..bb8dcb7 100644 --- a/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt +++ b/merchant-terminal/src/test/java/net/taler/merchantpos/order/OrderManagerTest.kt @@ -20,7 +20,7 @@ import android.app.Application import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.runBlocking -import net.taler.lib.common.Amount +import net.taler.common.Amount import net.taler.merchantlib.MerchantConfig import net.taler.merchantpos.R import net.taler.merchantpos.config.Category diff --git a/multiplatform b/multiplatform deleted file mode 160000 -Subproject cb92566166884efdaad5e93ef25e2de1f303461 diff --git a/taler-kotlin-android/build.gradle b/taler-kotlin-android/build.gradle index e47efe9..b4590c4 100644 --- a/taler-kotlin-android/build.gradle +++ b/taler-kotlin-android/build.gradle @@ -58,8 +58,6 @@ android { } dependencies { - api project(":multiplatform:common") - implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.core:core-ktx:1.7.0' implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" diff --git a/taler-kotlin-android/src/main/AndroidManifest.xml b/taler-kotlin-android/src/main/AndroidManifest.xml index c1f6146..f475748 100644 --- a/taler-kotlin-android/src/main/AndroidManifest.xml +++ b/taler-kotlin-android/src/main/AndroidManifest.xml @@ -18,9 +18,6 @@ xmlns:tools="http://schemas.android.com/tools"> <uses-sdk tools:overrideLibrary="com.google.zxing.client.android" /> - <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> - <uses-permission android:name="android.permission.INTERNET" /> - <uses-permission android:name="android.permission.NFC" /> </manifest> diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt new file mode 100644 index 0000000..a36c0ab --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt @@ -0,0 +1,199 @@ +/* + * 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 kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.math.floor +import kotlin.math.pow +import kotlin.math.roundToInt + +public class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) +public class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) + +@Serializable(with = KotlinXAmountSerializer::class) +public data class Amount( + /** + * name of the currency using either a three-character ISO 4217 currency code, + * or a regional currency identifier starting with a "*" followed by at most 10 characters. + * ISO 4217 exponents in the name are not supported, + * although the "fraction" is corresponds to an ISO 4217 exponent of 6. + */ + val currency: String, + + /** + * The integer part may be at most 2^52. + * Note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent. + */ + val value: Long, + + /** + * Unsigned 32 bit fractional value to be added to value representing + * an additional currency fraction, in units of one hundred millionth (1e-8) + * of the base currency value. For example, a fraction + * of 50_000_000 would correspond to 50 cents. + */ + val fraction: Int +) : Comparable<Amount> { + + public companion object { + + private const val FRACTIONAL_BASE: Int = 100000000 // 1e8 + + private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""") + public val MAX_VALUE: Long = 2.0.pow(52).toLong() + private const val MAX_FRACTION_LENGTH = 8 + public const val MAX_FRACTION: Int = 99_999_999 + + public fun zero(currency: String): Amount { + return Amount(checkCurrency(currency), 0, 0) + } + + public fun fromJSONString(str: String): Amount { + val split = str.split(":") + if (split.size != 2) throw AmountParserException("Invalid Amount Format") + return fromString(split[0], split[1]) + } + + public fun fromString(currency: String, str: String): Amount { + // value + val valueSplit = str.split(".") + val value = checkValue(valueSplit[0].toLongOrNull()) + // fraction + val fraction: Int = if (valueSplit.size > 1) { + val fractionStr = valueSplit[1] + if (fractionStr.length > MAX_FRACTION_LENGTH) + throw AmountParserException("Fraction $fractionStr too long") + val fraction = "0.$fractionStr".toDoubleOrNull() + ?.times(FRACTIONAL_BASE) + ?.roundToInt() + checkFraction(fraction) + } else 0 + return Amount(checkCurrency(currency), value, fraction) + } + + public fun min(currency: String): Amount = Amount(currency, 0, 1) + public fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION) + + + internal fun checkCurrency(currency: String): String { + if (!REGEX_CURRENCY.matches(currency)) + throw AmountParserException("Invalid currency: $currency") + return currency + } + + internal fun checkValue(value: Long?): Long { + if (value == null || value > MAX_VALUE) + throw AmountParserException("Value $value greater than $MAX_VALUE") + return value + } + + internal fun checkFraction(fraction: Int?): Int { + if (fraction == null || fraction > MAX_FRACTION) + throw AmountParserException("Fraction $fraction greater than $MAX_FRACTION") + return fraction + } + + } + + public val amountStr: String + get() = if (fraction == 0) "$value" else { + var f = fraction + var fractionStr = "" + while (f > 0) { + fractionStr += f / (FRACTIONAL_BASE / 10) + f = (f * 10) % FRACTIONAL_BASE + } + "$value.$fractionStr" + } + + public operator fun plus(other: Amount): Amount { + check(currency == other.currency) { "Can only subtract from same currency" } + val resultValue = value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong() + if (resultValue > MAX_VALUE) + throw AmountOverflowException() + val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE + return Amount(currency, resultValue, resultFraction) + } + + public operator fun times(factor: Int): Amount { + // TODO consider replacing with a faster implementation + if (factor == 0) return zero(currency) + var result = this + for (i in 1 until factor) result += this + return result + } + + public operator fun minus(other: Amount): Amount { + check(currency == other.currency) { "Can only subtract from same currency" } + var resultValue = value + var resultFraction = fraction + if (resultFraction < other.fraction) { + if (resultValue < 1L) + throw AmountOverflowException() + resultValue-- + resultFraction += FRACTIONAL_BASE + } + check(resultFraction >= other.fraction) + resultFraction -= other.fraction + if (resultValue < other.value) + throw AmountOverflowException() + resultValue -= other.value + return Amount(currency, resultValue, resultFraction) + } + + public fun isZero(): Boolean { + return value == 0L && fraction == 0 + } + + public fun toJSONString(): String { + return "$currency:$amountStr" + } + + override fun toString(): String { + return "$amountStr $currency" + } + + override fun compareTo(other: Amount): Int { + check(currency == other.currency) { "Can only compare amounts with the same currency" } + when { + value == other.value -> { + if (fraction < other.fraction) return -1 + if (fraction > other.fraction) return 1 + return 0 + } + value < other.value -> return -1 + else -> return 1 + } + } + +} + +@Suppress("EXPERIMENTAL_API_USAGE") +@Serializer(forClass = Amount::class) +internal object KotlinXAmountSerializer : KSerializer<Amount> { + override fun serialize(encoder: Encoder, value: Amount) { + encoder.encodeString(value.toJSONString()) + } + + override fun deserialize(decoder: Decoder): Amount { + return Amount.fromJSONString(decoder.decodeString()) + } +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt index 4ac2e73..d86e744 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt @@ -49,7 +49,6 @@ import androidx.navigation.fragment.findNavController import com.github.pedrovgs.lynx.LynxActivity import com.github.pedrovgs.lynx.LynxConfig import net.taler.lib.android.ErrorBottomSheet -import net.taler.lib.common.Version fun View.fadeIn(endAction: () -> Unit = {}) { if (visibility == VISIBLE && alpha == 1f) return diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt index fb30692..910cc36 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt @@ -20,8 +20,6 @@ import android.os.Build import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.taler.common.TalerUtils.getLocalizedString -import net.taler.lib.common.Amount -import net.taler.lib.common.Timestamp @Serializable data class ContractTerms( diff --git a/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt b/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt new file mode 100644 index 0000000..a1b7225 --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt @@ -0,0 +1,62 @@ +/* + * 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 java.util.Locale + +public object TalerUri { + + private const val SCHEME = "taler://" + private const val SCHEME_INSECURE = "taler+http://" + private const val AUTHORITY_PAY = "pay" + private const val AUTHORITY_WITHDRAW = "withdraw" + private const val AUTHORITY_REFUND = "refund" + private const val AUTHORITY_TIP = "tip" + + public data class WithdrawUriResult( + val bankIntegrationApiBaseUrl: String, + val withdrawalOperationId: String + ) + + /** + * Parses a withdraw URI and returns a bank status URL or null if the URI was invalid. + */ + public fun parseWithdrawUri(uri: String): WithdrawUriResult? { + val (resultScheme, prefix) = when { + uri.startsWith(SCHEME, ignoreCase = true) -> { + Pair("https://", "${SCHEME}${AUTHORITY_WITHDRAW}/") + } + uri.startsWith(SCHEME_INSECURE, ignoreCase = true) -> { + Pair("http://", "${SCHEME_INSECURE}${AUTHORITY_WITHDRAW}/") + } + else -> return null + } + if (!uri.startsWith(prefix)) return null + val parts = uri.let { + (if (it.endsWith("/")) it.dropLast(1) else it).substring(prefix.length).split('/') + } + if (parts.size < 2) return null + val host = parts[0].lowercase(Locale.ROOT) + val pathSegments = parts.slice(1 until parts.size - 1).joinToString("/") + val withdrawId = parts.last() + if (withdrawId.isBlank()) return null + val url = "${resultScheme}${host}/${pathSegments}" + + return WithdrawUriResult(url, withdrawId) + } + +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Time.kt b/taler-kotlin-android/src/main/java/net/taler/common/Time.kt new file mode 100644 index 0000000..61bbce8 --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/Time.kt @@ -0,0 +1,129 @@ +/* + * 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 kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.JsonTransformingSerializer +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import kotlin.math.max + +@Serializable +data class Timestamp( + @SerialName("t_ms") + @Serializable(NeverSerializer::class) + val old_ms: Long? = null, + @SerialName("t_s") + @Serializable(NeverSerializer::class) + private val s: Long? = null, +) : Comparable<Timestamp> { + + constructor(ms: Long) : this(ms, null) + + companion object { + private const val NEVER: Long = -1 + fun now(): Timestamp = Timestamp(System.currentTimeMillis()) + fun never(): Timestamp = Timestamp(NEVER) + } + + val ms: Long = if (s != null) { + s * 1000L + } else if (old_ms !== null) { + old_ms + } else { + throw Exception("timestamp didn't have t_s or t_ms") + } + + + /** + * Returns a copy of this [Timestamp] rounded to seconds. + */ + fun truncateSeconds(): Timestamp { + if (ms == NEVER) return Timestamp(ms) + return Timestamp((ms / 1000L) * 1000L) + } + + operator fun minus(other: Timestamp): Duration = when { + ms == NEVER -> Duration(Duration.FOREVER) + other.ms == NEVER -> throw Error("Invalid argument for timestamp comparision") + ms < other.ms -> Duration(0) + else -> Duration(ms - other.ms) + } + + operator fun minus(other: Duration): Timestamp = when { + ms == NEVER -> this + other.ms == Duration.FOREVER -> Timestamp(0) + else -> Timestamp(max(0, ms - other.ms)) + } + + override fun compareTo(other: Timestamp): Int { + return if (ms == NEVER) { + if (other.ms == NEVER) 0 + else 1 + } else { + if (other.ms == NEVER) -1 + else ms.compareTo(other.ms) + } + } + +} + +@Serializable +data class Duration( + @SerialName("d_ms") + @Serializable(ForeverSerializer::class) val old_ms: Long? = null, + @SerialName("d_s") + @Serializable(ForeverSerializer::class) + private val s: Long? = null, +) { + val ms: Long = if (s != null) { + s * 1000L + } else if (old_ms !== null) { + old_ms + } else { + throw Exception("duration didn't have d_s or d_ms") + } + + constructor(ms: Long) : this(ms, null) + + companion object { + internal const val FOREVER: Long = -1 + fun forever(): Duration = Duration(FOREVER) + } +} + +internal abstract class MinusOneSerializer(private val keyword: String) : + JsonTransformingSerializer<Long>(Long.serializer()) { + + override fun transformDeserialize(element: JsonElement): JsonElement { + return if (element.jsonPrimitive.contentOrNull == keyword) return JsonPrimitive(-1) + else super.transformDeserialize(element) + } + + override fun transformSerialize(element: JsonElement): JsonElement { + return if (element.jsonPrimitive.longOrNull == -1L) return JsonPrimitive(keyword) + else element + } +} + +internal object NeverSerializer : MinusOneSerializer("never") +internal object ForeverSerializer : MinusOneSerializer("forever") diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Version.kt b/taler-kotlin-android/src/main/java/net/taler/common/Version.kt new file mode 100644 index 0000000..f46913e --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/Version.kt @@ -0,0 +1,70 @@ +/* + * 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 kotlin.math.sign + +/** + * Semantic versioning, but libtool-style. + * See https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html + */ +public data class Version( + val current: Int, + val revision: Int, + val age: Int +) { + public companion object { + public fun parse(v: String): Version? { + val elements = v.split(":") + if (elements.size != 3) return null + val (currentStr, revisionStr, ageStr) = elements + val current = currentStr.toIntOrNull() + val revision = revisionStr.toIntOrNull() + val age = ageStr.toIntOrNull() + if (current == null || revision == null || age == null) return null + return Version(current, revision, age) + } + } + + /** + * Compare two libtool-style versions. + * + * Returns a [VersionMatchResult] or null if the given version was null. + */ + public fun compare(other: Version?): VersionMatchResult? { + if (other == null) return null + val compatible = current - age <= other.current && + current >= other.current - other.age + val currentCmp = sign((current - other.current).toDouble()).toInt() + return VersionMatchResult(compatible, currentCmp) + } + + /** + * Result of comparing two libtool versions. + */ + public data class VersionMatchResult( + /** + * Is the first version compatible with the second? + */ + val compatible: Boolean, + /** + * Is the first version older (-1), newer (+1) or identical (0)? + */ + val currentCmp: Int + ) + +} diff --git a/taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt new file mode 100644 index 0000000..3343b52 --- /dev/null +++ b/taler-kotlin-android/src/test/java/net/taler/common/AmountTest.kt @@ -0,0 +1,221 @@ +/* + * 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 kotlin.random.Random +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.assertFalse +import org.junit.Test + +class AmountTest { + + companion object { + fun getRandomAmount() = getRandomAmount(getRandomString(1, Random.nextInt(1, 12))) + fun getRandomAmount(currency: String): Amount { + val value = Random.nextLong(0, Amount.MAX_VALUE) + val fraction = Random.nextInt(0, Amount.MAX_FRACTION) + return Amount(currency, value, fraction) + } + } + + @Test + fun testFromJSONString() { + var str = "TESTKUDOS:23.42" + var amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("TESTKUDOS", amount.currency) + assertEquals(23, amount.value) + assertEquals((0.42 * 1e8).toInt(), amount.fraction) + assertEquals("23.42 TESTKUDOS", amount.toString()) + + str = "EUR:500000000.00000001" + amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("EUR", amount.currency) + assertEquals(500000000, amount.value) + assertEquals(1, amount.fraction) + assertEquals("500000000.00000001 EUR", amount.toString()) + + str = "EUR:1500000000.00000003" + amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("EUR", amount.currency) + assertEquals(1500000000, amount.value) + assertEquals(3, amount.fraction) + assertEquals("1500000000.00000003 EUR", amount.toString()) + } + + @Test + fun testFromJSONStringAcceptsMaxValuesRejectsAbove() { + val maxValue = 4503599627370496 + val str = "TESTKUDOS123:$maxValue.99999999" + val amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("TESTKUDOS123", amount.currency) + assertEquals(maxValue, amount.value) + assertEquals("$maxValue.99999999 TESTKUDOS123", amount.toString()) + + // longer currency not accepted + assertThrows<AmountParserException>("longer currency was accepted") { + Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999") + } + + // max value + 1 not accepted + assertThrows<AmountParserException>("max value + 1 was accepted") { + Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999") + } + + // max fraction + 1 not accepted + assertThrows<AmountParserException>("max fraction + 1 was accepted") { + Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990") + } + } + + @Test + fun testFromJSONStringRejections() { + assertThrows<AmountParserException> { + Amount.fromJSONString("TESTKUDOS:0,5") + } + assertThrows<AmountParserException> { + Amount.fromJSONString("+TESTKUDOS:0.5") + } + assertThrows<AmountParserException> { + Amount.fromJSONString("0.5") + } + assertThrows<AmountParserException> { + Amount.fromJSONString(":0.5") + } + assertThrows<AmountParserException> { + Amount.fromJSONString("EUR::0.5") + } + assertThrows<AmountParserException> { + Amount.fromJSONString("EUR:.5") + } + } + + @Test + fun testAddition() { + assertEquals( + Amount.fromJSONString("EUR:2"), + Amount.fromJSONString("EUR:1") + Amount.fromJSONString("EUR:1") + ) + assertEquals( + Amount.fromJSONString("EUR:3"), + Amount.fromJSONString("EUR:1.5") + Amount.fromJSONString("EUR:1.5") + ) + assertEquals( + Amount.fromJSONString("EUR:500000000.00000002"), + Amount.fromJSONString("EUR:500000000.00000001") + Amount.fromJSONString("EUR:0.00000001") + ) + assertThrows<AmountOverflowException>("addition didn't overflow") { + Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001") + } + assertThrows<AmountOverflowException>("addition didn't overflow") { + Amount.fromJSONString("EUR:4000000000000000") + Amount.fromJSONString("EUR:4000000000000000") + } + } + + @Test + fun testTimes() { + assertEquals( + Amount.fromJSONString("EUR:2"), + Amount.fromJSONString("EUR:2") * 1 + ) + assertEquals( + Amount.fromJSONString("EUR:2"), + Amount.fromJSONString("EUR:1") * 2 + ) + assertEquals( + Amount.fromJSONString("EUR:4.5"), + Amount.fromJSONString("EUR:1.5") * 3 + ) + assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:1.11") * 0) + assertEquals(Amount.fromJSONString("EUR:1.11"), Amount.fromJSONString("EUR:1.11") * 1) + assertEquals(Amount.fromJSONString("EUR:2.22"), Amount.fromJSONString("EUR:1.11") * 2) + assertEquals(Amount.fromJSONString("EUR:3.33"), Amount.fromJSONString("EUR:1.11") * 3) + assertEquals(Amount.fromJSONString("EUR:4.44"), Amount.fromJSONString("EUR:1.11") * 4) + assertEquals(Amount.fromJSONString("EUR:5.55"), Amount.fromJSONString("EUR:1.11") * 5) + assertEquals( + Amount.fromJSONString("EUR:1500000000.00000003"), + Amount.fromJSONString("EUR:500000000.00000001") * 3 + ) + assertThrows<AmountOverflowException>("times didn't overflow") { + Amount.fromJSONString("EUR:4000000000000000") * 2 + } + } + + @Test + fun testSubtraction() { + assertEquals( + Amount.fromJSONString("EUR:0"), + Amount.fromJSONString("EUR:1") - Amount.fromJSONString("EUR:1") + ) + assertEquals( + Amount.fromJSONString("EUR:1.5"), + Amount.fromJSONString("EUR:3") - Amount.fromJSONString("EUR:1.5") + ) + assertEquals( + Amount.fromJSONString("EUR:500000000.00000001"), + Amount.fromJSONString("EUR:500000000.00000002") - Amount.fromJSONString("EUR:0.00000001") + ) + assertThrows<AmountOverflowException>("subtraction didn't underflow") { + Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23") + } + assertThrows<AmountOverflowException>("subtraction didn't underflow") { + Amount.fromJSONString("EUR:0.5") - Amount.fromJSONString("EUR:0.50000001") + } + } + + @Test + fun testIsZero() { + assertTrue(Amount.zero("EUR").isZero()) + assertTrue(Amount.fromJSONString("EUR:0").isZero()) + assertTrue(Amount.fromJSONString("EUR:0.0").isZero()) + assertTrue(Amount.fromJSONString("EUR:0.00000").isZero()) + assertTrue((Amount.fromJSONString("EUR:1.001") - Amount.fromJSONString("EUR:1.001")).isZero()) + + assertFalse(Amount.fromJSONString("EUR:0.00000001").isZero()) + assertFalse(Amount.fromJSONString("EUR:1.0").isZero()) + assertFalse(Amount.fromJSONString("EUR:0001.0").isZero()) + } + + @Test + fun testComparision() { + assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0")) + assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:1")) + assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:0")) + assertEquals(Amount.fromJSONString("EUR:42"), Amount.fromJSONString("EUR:42")) + assertEquals( + Amount.fromJSONString("EUR:42.00000001"), + Amount.fromJSONString("EUR:42.00000001") + ) + assertTrue(Amount.fromJSONString("EUR:42.00000001") >= Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:42.00000002") >= Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:42.00000002") > Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:0.00000002") > Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0.00000001") > Amount.fromJSONString("EUR:0")) + assertTrue(Amount.fromJSONString("EUR:2") > Amount.fromJSONString("EUR:1")) + + assertThrows<IllegalStateException>("could compare amounts with different currencies") { + Amount.fromJSONString("EUR:0.5") < Amount.fromJSONString("USD:0.50000001") + } + } + +} diff --git a/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt index 3a2cdb4..f7b83a9 100644 --- a/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt +++ b/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt @@ -18,7 +18,6 @@ package net.taler.common import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import net.taler.lib.common.Timestamp import org.junit.Assert.assertEquals import org.junit.Test diff --git a/taler-kotlin-android/src/test/java/net/taler/common/TalerUriTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/TalerUriTest.kt new file mode 100644 index 0000000..128f707 --- /dev/null +++ b/taler-kotlin-android/src/test/java/net/taler/common/TalerUriTest.kt @@ -0,0 +1,65 @@ +/* + * 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 net.taler.common.TalerUri.parseWithdrawUri +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class TalerUriTest { + + @Test + fun testParseWithdrawUri() { + // correct parsing + var uri = "taler://withdraw/bank.example.com/12345" + var expected = TalerUri.WithdrawUriResult("https://bank.example.com/", "12345") + assertEquals(expected, parseWithdrawUri(uri)) + + // correct parsing with insecure http + uri = "taler+http://withdraw/bank.example.org/foo" + expected = TalerUri.WithdrawUriResult("http://bank.example.org/", "foo") + assertEquals(expected, parseWithdrawUri(uri)) + + // correct parsing with long path + uri = "taler://withdraw/bank.example.com/foo/bar/23/42/1337/1234567890" + expected = + TalerUri.WithdrawUriResult("https://bank.example.com/foo/bar/23/42/1337", "1234567890") + assertEquals(expected, parseWithdrawUri(uri)) + + // rejects incorrect scheme + uri = "talerx://withdraw/bank.example.com/12345" + assertNull(parseWithdrawUri(uri)) + + // rejects incorrect authority + uri = "taler://withdrawx/bank.example.com/12345" + assertNull(parseWithdrawUri(uri)) + + // rejects incorrect authority with insecure http + uri = "taler+http://withdrawx/bank.example.com/12345" + assertNull(parseWithdrawUri(uri)) + + // rejects empty withdrawalId + uri = "taler://withdraw/bank.example.com//" + assertNull(parseWithdrawUri(uri)) + + // rejects empty path and withdrawalId + uri = "taler://withdraw/bank.example.com////" + assertNull(parseWithdrawUri(uri)) + } + +} diff --git a/taler-kotlin-android/src/test/java/net/taler/common/TestUtils.kt b/taler-kotlin-android/src/test/java/net/taler/common/TestUtils.kt new file mode 100644 index 0000000..b0f191d --- /dev/null +++ b/taler-kotlin-android/src/test/java/net/taler/common/TestUtils.kt @@ -0,0 +1,40 @@ +/* + * 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 kotlin.random.Random +import org.junit.Assert.assertTrue +import org.junit.Assert.fail + +private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') +fun getRandomString(minLength: Int = 1, maxLength: Int = Random.nextInt(0, 1337)) = + (minLength..maxLength) + .map { Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") + +inline fun <reified T : Throwable> assertThrows( + msg: String? = null, + function: () -> Any +) { + try { + function.invoke() + fail(msg) + } catch (e: Exception) { + assertTrue(e is T) + } +} diff --git a/taler-kotlin-android/src/test/java/net/taler/common/TimeTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/TimeTest.kt new file mode 100644 index 0000000..beda621 --- /dev/null +++ b/taler-kotlin-android/src/test/java/net/taler/common/TimeTest.kt @@ -0,0 +1,46 @@ +/* + * 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 kotlinx.serialization.json.Json.Default.decodeFromString +import kotlinx.serialization.json.Json.Default.encodeToString +import kotlin.random.Random +import org.junit.Assert.assertEquals +import org.junit.Test + +// TODO test other functionality of Timestamp and Duration +class TimeTest { + + @Test + fun testSerialize() { + for (i in 0 until 42) { + val t = Random.nextLong() + assertEquals("""{"t_ms":$t}""", encodeToString(Timestamp.serializer(), Timestamp(t))) + } + assertEquals("""{"t_ms":"never"}""", encodeToString(Timestamp.serializer(), Timestamp.never())) + } + + @Test + fun testDeserialize() { + for (i in 0 until 42) { + val t = Random.nextLong() + assertEquals(Timestamp(t), decodeFromString(Timestamp.serializer(), """{ "t_ms": $t }""")) + } + assertEquals(Timestamp.never(), decodeFromString(Timestamp.serializer(), """{ "t_ms": "never" }""")) + } + +} diff --git a/taler-kotlin-android/src/test/java/net/taler/common/VersionTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/VersionTest.kt new file mode 100644 index 0000000..d8d7149 --- /dev/null +++ b/taler-kotlin-android/src/test/java/net/taler/common/VersionTest.kt @@ -0,0 +1,65 @@ +/* + * 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 org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + + +class VersionTest { + + @Test + fun testParse() { + assertNull(Version.parse("")) + assertNull(Version.parse("foo")) + assertNull(Version.parse("foo:bar:foo")) + assertNull(Version.parse("0:0:0:")) + assertNull(Version.parse("0:0:")) + assertEquals(Version(0, 0, 0), Version.parse("0:0:0")) + assertEquals(Version(1, 2, 3), Version.parse("1:2:3")) + assertEquals(Version(1337, 42, 23), Version.parse("1337:42:23")) + } + + @Test + fun testComparision() { + assertEquals( + Version.VersionMatchResult(true, 0), + Version.parse("0:0:0")!!.compare(Version.parse("0:0:0")) + ) + assertEquals( + Version.VersionMatchResult(true, -1), + Version.parse("0:0:0")!!.compare(Version.parse("1:0:1")) + ) + assertEquals( + Version.VersionMatchResult(true, -1), + Version.parse("0:0:0")!!.compare(Version.parse("1:5:1")) + ) + assertEquals( + Version.VersionMatchResult(false, -1), + Version.parse("0:0:0")!!.compare(Version.parse("1:5:0")) + ) + assertEquals( + Version.VersionMatchResult(false, 1), + Version.parse("1:0:0")!!.compare(Version.parse("0:5:0")) + ) + assertEquals( + Version.VersionMatchResult(true, 0), + Version.parse("1:0:1")!!.compare(Version.parse("1:5:1")) + ) + } + +} diff --git a/wallet/build.gradle b/wallet/build.gradle index 20bdd49..b0a8b97 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -91,7 +91,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion '1.0.5' + kotlinCompilerExtensionVersion '1.1.1' } buildFeatures { @@ -121,7 +121,7 @@ dependencies { implementation project(":anastasis-ui") implementation 'net.taler:akono:0.2' - implementation "org.jetbrains.kotlin:kotlin-reflect:1.6.0" + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation 'androidx.preference:preference-ktx:1.2.0' implementation "com.google.android.material:material:$material_version" diff --git a/wallet/src/main/AndroidManifest.xml b/wallet/src/main/AndroidManifest.xml index 285908a..b0e4ffe 100644 --- a/wallet/src/main/AndroidManifest.xml +++ b/wallet/src/main/AndroidManifest.xml @@ -43,6 +43,7 @@ <activity android:name=".MainActivity" + android:exported="true" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt index 09ae353..24ee1a1 100644 --- a/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt +++ b/wallet/src/main/java/net/taler/wallet/balances/BalanceAdapter.kt @@ -25,7 +25,7 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter import kotlinx.serialization.Serializable -import net.taler.lib.common.Amount +import net.taler.common.Amount import net.taler.wallet.R import net.taler.wallet.balances.BalanceAdapter.BalanceViewHolder diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt index 1da9b49..7824d1e 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFees.kt @@ -16,8 +16,9 @@ package net.taler.wallet.exchanges -import net.taler.lib.common.Amount -import net.taler.lib.common.Timestamp +import net.taler.common.Amount +import net.taler.common.Timestamp + data class CoinFee( val coin: Amount, diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt index 1ea32dd..d8242f3 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeFeesFragment.kt @@ -27,9 +27,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.ViewHolder +import net.taler.common.Amount import net.taler.common.toRelativeTime import net.taler.common.toShortDate -import net.taler.lib.common.Amount import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentExchangeFeesBinding diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt index 34a8023..ae091df 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -22,8 +22,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import net.taler.common.Amount import net.taler.common.ContractTerms -import net.taler.lib.common.Amount import net.taler.wallet.TAG import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.backend.TalerErrorInfo diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt index 4b908b5..fb206be 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt @@ -18,9 +18,9 @@ package net.taler.wallet.payment import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.taler.common.Amount import net.taler.common.ContractTerms import net.taler.lib.android.CustomClassDiscriminator -import net.taler.lib.common.Amount import net.taler.wallet.backend.TalerErrorInfo @Serializable diff --git a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt index 700e158..d5c3eaf 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt @@ -28,10 +28,10 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG +import net.taler.common.Amount import net.taler.common.ContractTerms import net.taler.common.fadeIn import net.taler.common.fadeOut -import net.taler.lib.common.Amount import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentPromptPaymentBinding @@ -49,7 +49,7 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { ui = FragmentPromptPaymentBinding.inflate(inflater, container, false) return ui.root @@ -131,7 +131,7 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener { } } - private fun showOrder(contractTerms: ContractTerms, amount:Amount, totalFees: Amount? = null) { + private fun showOrder(contractTerms: ContractTerms, amount: Amount, totalFees: Amount? = null) { ui.details.orderView.text = contractTerms.summary adapter.setItems(contractTerms.products) ui.details.productsList.fadeIn() diff --git a/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt b/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt index 9c292aa..f3c41e8 100644 --- a/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt +++ b/wallet/src/main/java/net/taler/wallet/refund/RefundManager.kt @@ -21,7 +21,7 @@ import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.serialization.Serializable -import net.taler.lib.common.Amount +import net.taler.common.Amount import net.taler.wallet.backend.WalletBackendApi sealed class RefundStatus { diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt index 866b363..128ca04 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailFragment.kt @@ -25,8 +25,8 @@ import android.view.MenuItem import android.widget.TextView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import net.taler.common.Amount import net.taler.common.startActivitySafe -import net.taler.lib.common.Amount import net.taler.wallet.MainViewModel import net.taler.wallet.R diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt index 50181c5..cca370e 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -23,10 +23,10 @@ import androidx.annotation.StringRes import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import net.taler.common.Amount import net.taler.common.ContractMerchant import net.taler.common.ContractProduct -import net.taler.lib.common.Amount -import net.taler.lib.common.Timestamp +import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.cleanExchange diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt index 90510e6..e63d451 100644 --- a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsFragment.kt @@ -36,6 +36,7 @@ import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL +import net.taler.common.Amount import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.wallet.MainViewModel @@ -112,12 +113,12 @@ class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode. override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - model.balances.observe(viewLifecycleOwner, { balances -> - balances.find { it.currency == currency }?.available?.let { amount -> + model.balances.observe(viewLifecycleOwner) { balances -> + balances.find { it.currency == currency }?.available?.let { amount: Amount -> requireActivity().title = getString(R.string.transactions_detail_title_balance, amount) } - }) + } } override fun onSaveInstanceState(outState: Bundle) { diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt index e78ff44..0cb39d2 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt @@ -24,8 +24,8 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import net.taler.common.Amount import net.taler.common.hideKeyboard -import net.taler.lib.common.Amount import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.databinding.FragmentManualWithdrawBinding diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt index 4ea3e73..cdacde6 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt @@ -60,8 +60,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.composethemeadapter.MdcTheme +import net.taler.common.Amount import net.taler.common.startActivitySafe -import net.taler.lib.common.Amount import net.taler.wallet.MainViewModel import net.taler.wallet.R diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt index 08cbc2e..dfae920 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -25,10 +25,10 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG +import net.taler.common.Amount import net.taler.common.EventObserver import net.taler.common.fadeIn import net.taler.common.fadeOut -import net.taler.lib.common.Amount import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.cleanExchange diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt index fbb5c18..d96f421 100644 --- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt +++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -24,9 +24,9 @@ import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import net.taler.common.Amount import net.taler.common.Event import net.taler.common.toEvent -import net.taler.lib.common.Amount import net.taler.wallet.TAG import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi |