package cy.agorise.bitsybitshareswallet.fragments import android.content.Intent import android.content.pm.PackageManager import android.graphics.drawable.Animatable import android.os.Bundle import android.preference.PreferenceManager import android.util.Log import android.view.* import android.widget.AdapterView import androidx.appcompat.widget.Toolbar import androidx.collection.LongSparseArray import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import com.crashlytics.android.Crashlytics import com.google.common.primitives.UnsignedLong import com.jakewharton.rxbinding3.widget.textChanges import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.adapters.AssetsAdapter import cy.agorise.bitsybitshareswallet.adapters.AutoSuggestAssetAdapter import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Helper import cy.agorise.bitsybitshareswallet.utils.toast import cy.agorise.bitsybitshareswallet.viewmodels.ReceiveTransactionViewModel import cy.agorise.graphenej.* import cy.agorise.graphenej.api.ConnectionStatusUpdate import cy.agorise.graphenej.api.calls.ListAssets import cy.agorise.graphenej.models.JsonRpcResponse import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_receive_transaction.* import java.lang.Exception import java.math.RoundingMode import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.* import java.util.concurrent.TimeUnit import kotlin.collections.ArrayList import kotlin.math.min class ReceiveTransactionFragment : ConnectedFragment() { companion object { private const val TAG = "ReceiveTransactionFrag" private const val RESPONSE_LIST_ASSETS = 1 private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100 /** Number of assets to request from the NetworkService to show as suggestions in the AutoCompleteTextView */ private const val AUTO_SUGGEST_ASSET_LIMIT = 5 private const val OTHER_ASSET = "other_asset" } private lateinit var mViewModel: ReceiveTransactionViewModel /** Current user account */ private var mUserAccount: UserAccount? = null private var mAsset: Asset? = null private var mAssetsAdapter: AssetsAdapter? = null private lateinit var mAutoSuggestAssetAdapter: AutoSuggestAssetAdapter private var mAssets = ArrayList() /** Keeps track of the current selected asset symbol */ private var selectedAssetSymbol = "BTS" /** Used to avoid erasing the QR code when the user selects an item from the AutoComplete suggestions */ private var selectedInAutoCompleteTextView = false // Map used to keep track of request and response id pairs private val responseMap = LongSparseArray() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { setHasOptionsMenu(true) val nightMode = PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(Constants.KEY_NIGHT_MODE_ACTIVATED, false) // Sets the toolbar background color to green val toolbar: Toolbar? = activity?.findViewById(R.id.toolbar) toolbar?.setBackgroundResource(if (!nightMode) R.color.colorReceive else R.color.colorToolbarDark) // Sets the status and navigation bars background color to a dark green or just dark val window = activity?.window context?.let { context -> val statusBarColor = ContextCompat.getColor(context, if (!nightMode) R.color.colorReceiveDark else R.color.colorStatusBarDark) window?.statusBarColor = statusBarColor window?.navigationBarColor = statusBarColor } return inflater.inflate(R.layout.fragment_receive_transaction, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Crashlytics.setString(Constants.CRASHLYTICS_KEY_LAST_SCREEN, TAG) // Configure ViewModel mViewModel = ViewModelProviders.of(this).get(ReceiveTransactionViewModel::class.java) val userId = PreferenceManager.getDefaultSharedPreferences(context) .getString(Constants.KEY_CURRENT_ACCOUNT_ID, "") mViewModel.getUserAccount(userId!!).observe(this, Observer{ user -> mUserAccount = UserAccount(user.id, user.name) }) mViewModel.getAllNonZero().observe(this, Observer> { assets -> mAssets.clear() mAssets.addAll(assets) // Add BTS to always show a QR if (mAssets.isEmpty()) mAssets.add(cy.agorise.bitsybitshareswallet.database.entities.Asset( "1.3.0", "BTS", 5, "", "") ) mAssets.sortWith( Comparator { a, b -> a.toString().compareTo(b.toString(), true) } ) // Add an option at the end so the user can search for an asset other than the ones saved in the db val asset = cy.agorise.bitsybitshareswallet.database.entities.Asset( OTHER_ASSET, getString(R.string.text__other), 0, "", "" ) mAssets.add(asset) mAssetsAdapter = AssetsAdapter(context!!, android.R.layout.simple_spinner_item, mAssets) spAsset.adapter = mAssetsAdapter // Try to select the selectedAssetSymbol for (i in 0 until mAssetsAdapter!!.count) { if (mAssetsAdapter!!.getItem(i)!!.symbol == selectedAssetSymbol) { spAsset.setSelection(i) break } } }) mViewModel.qrCodeBitmap.observe(this, Observer { bitmap -> ivQR.setImageBitmap(bitmap) }) spAsset.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{ override fun onNothingSelected(parent: AdapterView<*>?) { } override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { mAssetsAdapter?.getItem(position)?.let { asset -> if (asset.id == OTHER_ASSET) { tilAsset.visibility = View.VISIBLE mAsset = null } else { tilAsset.visibility = View.GONE selectedAssetSymbol = asset.symbol mAsset = Asset(asset.id, asset.toString(), asset.precision) } } updateQR() } } // Use RxJava Debounce to create QR code only after the user stopped typing an amount mDisposables.add( tietAmount.textChanges() .debounce(1000, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { updateQR() }, { mAsset = null} ) ) // Add adapter to the Assets AutoCompleteTextView mAutoSuggestAssetAdapter = AutoSuggestAssetAdapter(context!!, android.R.layout.simple_dropdown_item_1line) actvAsset.setAdapter(mAutoSuggestAssetAdapter) // Use RxJava Debounce to avoid making calls to the NetworkService on every text change event and also avoid // the first call when the View is created mDisposables.add( actvAsset.textChanges() .skipInitialValue() .debounce(500, TimeUnit.MILLISECONDS) .map { it.toString().trim().toUpperCase() } .observeOn(AndroidSchedulers.mainThread()) .subscribe( { if (!selectedInAutoCompleteTextView) { mAsset = null updateQR() } selectedInAutoCompleteTextView = false // Get a list of assets that match the already typed string by the user if (it.length > 1 && mNetworkService != null) { val id = mNetworkService?.sendMessage(ListAssets(it, AUTO_SUGGEST_ASSET_LIMIT), ListAssets.REQUIRED_API) if (id != null) responseMap.append(id, RESPONSE_LIST_ASSETS) } }, { mAsset = null } ) ) actvAsset.setOnItemClickListener { parent, _, position, _ -> val asset = parent.adapter.getItem(position) as cy.agorise.bitsybitshareswallet.database.entities.Asset mAsset = Asset(asset.id, asset.toString(), asset.precision) selectedInAutoCompleteTextView = true updateQR() } } override fun handleJsonRpcResponse(response: JsonRpcResponse<*>) { if (responseMap.containsKey(response.id)) { val responseType = responseMap[response.id] when (responseType) { RESPONSE_LIST_ASSETS -> handleListAssets(response.result) } responseMap.remove(response.id) } } override fun handleConnectionStatusUpdate(connectionStatusUpdate: ConnectionStatusUpdate) { if (connectionStatusUpdate.updateCode == ConnectionStatusUpdate.DISCONNECTED) { // If we got a disconnection notification, we should clear our response map, since // all its stored request ids will now be reset responseMap.clear() } } /** * Handles the list of assets returned from the node that correspond to what the user has typed in the Asset * AutoCompleteTextView and adds them to its adapter to show as suggestions. */ private fun handleListAssets(result: Any?) { if (result is List<*> && result.isNotEmpty() && result[0] is Asset) { val assetList = result as List Log.d(TAG, "handleListAssets") val assets = ArrayList() for (_asset in assetList) { val asset = cy.agorise.bitsybitshareswallet.database.entities.Asset( _asset.objectId, _asset.symbol, _asset.precision, _asset.description ?: "", _asset.issuer ?: "" ) assets.add(asset) } mAutoSuggestAssetAdapter.setData(assets) mAutoSuggestAssetAdapter.notifyDataSetChanged() } } private fun updateQR() { if (mAsset == null) { ivQR.setImageDrawable(null) // TODO clean the please pay and to text at the bottom too return } val asset = mAsset!! // Try to obtain the amount from the Amount Text Field or make it zero otherwise val amount: Long = try { val tmpAmount = tietAmount.text.toString().toDouble() (tmpAmount * Math.pow(10.0, asset.precision.toDouble())).toLong() }catch (e: Exception) { 0 } val total = AssetAmount(UnsignedLong.valueOf(amount), asset) val totalInDouble = Util.fromBase(total) val items = arrayOf(LineItem("transfer", 1, totalInDouble)) val invoice = Invoice(mUserAccount?.name, "", "", asset.symbol.replaceFirst("bit", ""), items, "", "") Log.d(TAG, "invoice: " + invoice.toJsonString()) try { mViewModel.updateInvoice(invoice, min(ivQR.width, ivQR.height)) updateAmountAddressUI(amount, asset.symbol, asset.precision, mUserAccount!!.name) } catch (e: NullPointerException) { Log.e(TAG, "NullPointerException. Msg: " + e.message) Crashlytics.logException(e) } } /** * Updates the UI to show the amount and account to send the payment */ private fun updateAmountAddressUI(assetAmount: Long, assetSymbol: String, assetPrecision: Int, account: String) { val txtAmount: String = if (assetAmount == 0L) { getString(R.string.template__please_send, getString(R.string.text__any_amount), " ") } else { val df = DecimalFormat("####."+("#".repeat(assetPrecision))) df.roundingMode = RoundingMode.CEILING df.decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()) val amount = assetAmount.toDouble() / Math.pow(10.toDouble(), assetPrecision.toDouble()) val strAmount = df.format(amount) getString(R.string.template__please_send, strAmount, assetSymbol) } val txtAccount = getString(R.string.template__to, account) tvPleasePay.text = txtAmount tvTo.text = txtAccount } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.menu_receive_transaction, menu) // Animate the share icon val shareIcon = menu.findItem(R.id.menu_share).icon if (shareIcon is Animatable) { shareIcon.start() } } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.menu_share) { verifyStoragePermission() return true } return super.onOptionsItemSelected(item) } private fun verifyStoragePermission() { if (ContextCompat.checkSelfPermission(activity!!, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Permission is not already granted requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) } else { // Permission is already granted shareQRScreenshot() } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) { if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { shareQRScreenshot() } else { context?.toast(getString(R.string.msg__storage_permission_necessary_share)) } return } } /** * This function takes a screenshot as a bitmap, saves it into a temporal cache image and then * sends an intent so the user can select the desired method to share the image. */ private fun shareQRScreenshot() { // TODO improve, show errors where necessary so the user can fix it // Avoid sharing the QR code image if the fields are not filled correctly if (mAsset == null) return // Get Screenshot val screenshot = Helper.loadBitmapFromView(container) val imageUri = Helper.saveTemporalBitmap(context!!, screenshot) // Prepare information for share intent val subject = getString(R.string.msg__invoice_subject, mUserAccount?.name) val content = tvPleasePay.text.toString() + "\n" + tvTo.text.toString() // Create share intent and call it val shareIntent = Intent() shareIntent.action = Intent.ACTION_SEND shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // temp permission for receiving app to read this file shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject) shareIntent.putExtra(Intent.EXTRA_TEXT, content) shareIntent.type = "*/*" startActivity(Intent.createChooser(shareIntent, getString(R.string.text__share_with))) } }