From b0efdca928f65bc96caf9b607de700d964296af9 Mon Sep 17 00:00:00 2001 From: m2049r <30435443+m2049r@users.noreply.github.com> Date: Fri, 18 Aug 2017 13:51:24 +0200 Subject: [PATCH] send gui + first successful send transaction! --- app/src/main/cpp/monerujo.cpp | 98 ++++++- .../m2049r/xmrwallet/GenerateFragment.java | 7 + .../com/m2049r/xmrwallet/LoginActivity.java | 7 +- .../com/m2049r/xmrwallet/SendFragment.java | 241 ++++++++++++++++++ .../com/m2049r/xmrwallet/WalletActivity.java | 99 ++++++- .../com/m2049r/xmrwallet/WalletFragment.java | 61 +++-- .../xmrwallet/model/PendingTransaction.java | 86 +++++++ .../com/m2049r/xmrwallet/model/Wallet.java | 34 ++- .../xmrwallet/service/WalletService.java | 40 +++ .../com/m2049r/xmrwallet/util/TxData.java | 80 ++++++ app/src/main/res/layout/gen_fragment.xml | 8 +- app/src/main/res/layout/send_fragment.xml | 210 +++++++++++++++ app/src/main/res/layout/wallet_fragment.xml | 72 ++++-- app/src/main/res/values/strings.xml | 34 ++- 14 files changed, 1013 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/com/m2049r/xmrwallet/SendFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/TxData.java create mode 100644 app/src/main/res/layout/send_fragment.xml diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp index 25069939..57176b76 100644 --- a/app/src/main/cpp/monerujo.cpp +++ b/app/src/main/cpp/monerujo.cpp @@ -389,7 +389,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_setDaemonAddressJ(JNIEnv *env, job // returns whether the daemon can be reached, and its version number JNIEXPORT jint JNICALL Java_com_m2049r_xmrwallet_model_WalletManager_getDaemonVersion(JNIEnv *env, - jobject instance) { + jobject instance) { uint32_t version; bool isConnected = Bitmonero::WalletManagerFactory::getWalletManager()->connected(&version); @@ -597,7 +597,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_getFilename(JNIEnv *env, jobject instance JNIEXPORT jboolean JNICALL Java_com_m2049r_xmrwallet_model_Wallet_initJ(JNIEnv *env, jobject instance, jstring daemon_address, - long upper_transaction_size_limit) { + jlong upper_transaction_size_limit) { // const std::string &daemon_username = "", const std::string &daemon_password = "") = 0; const char *_daemon_address = env->GetStringUTFChars(daemon_address, JNI_FALSE); Bitmonero::Wallet *wallet = getHandle(env, instance); @@ -757,16 +757,44 @@ Java_com_m2049r_xmrwallet_model_Wallet_refreshAsync(JNIEnv *env, jobject instanc //TODO virtual void setAutoRefreshInterval(int millis) = 0; //TODO virtual int autoRefreshInterval() const = 0; +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_createTransactionJ(JNIEnv *env, jobject instance, + jstring dst_addr, jstring payment_id, + jlong amount, jint mixin_count, + jint priority) { -//virtual PendingTransaction * createTransaction(const std::string &dst_addr, const std::string &payment_id, -// optional tvAmount, uint32_t mixin_count, -// PendingTransaction::Priority = PendingTransaction::Priority_Low) = 0; + const char *_dst_addr = env->GetStringUTFChars(dst_addr, JNI_FALSE); + const char *_payment_id = env->GetStringUTFChars(payment_id, JNI_FALSE); + Bitmonero::PendingTransaction::Priority _priority = + static_cast(priority); + + LOGD("Priority_Last is %i", static_cast(Bitmonero::PendingTransaction::Priority_Last)); + + Bitmonero::Wallet *wallet = getHandle(env, instance); + + Bitmonero::PendingTransaction *tx = wallet->createTransaction(_dst_addr, _payment_id, + amount, mixin_count, + _priority); + + env->ReleaseStringUTFChars(dst_addr, _dst_addr); + env->ReleaseStringUTFChars(payment_id, _payment_id); + return reinterpret_cast(tx); +} //virtual PendingTransaction * createSweepUnmixableTransaction() = 0; //virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0; //virtual bool submitTransaction(const std::string &fileName) = 0; -//virtual void disposeTransaction(PendingTransaction * t) = 0; + +JNIEXPORT void JNICALL +Java_com_m2049r_xmrwallet_model_Wallet_disposeTransaction(JNIEnv *env, jobject instance, + jobject pendingTransaction) { + Bitmonero::Wallet *wallet = getHandle(env, instance); + Bitmonero::PendingTransaction *_pendingTransaction = + getHandle(env, pendingTransaction); + wallet->disposeTransaction(_pendingTransaction); +} + //virtual bool exportKeyImages(const std::string &filename) = 0; //virtual bool importKeyImages(const std::string &filename) = 0; @@ -1002,6 +1030,64 @@ Java_com_m2049r_xmrwallet_model_TransactionInfo_getTransferCount(JNIEnv *env, jo } +JNIEXPORT jint JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getStatusJ(JNIEnv *env, jobject instance) { + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + return tx->status(); +} + +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getErrorString(JNIEnv *env, jobject instance) { + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + return env->NewStringUTF(tx->errorString().c_str()); +} + +// commit transaction or save to file if filename is provided. +JNIEXPORT jboolean JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_commit(JNIEnv *env, jobject instance, + jstring filename, jboolean overwrite) { + + const char *_filename = env->GetStringUTFChars(filename, JNI_FALSE); + + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + bool success = tx->commit(_filename, overwrite); + + env->ReleaseStringUTFChars(filename, _filename); + return success; +} + + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getAmount(JNIEnv *env, jobject instance) { + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + return tx->amount(); +} +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getDust(JNIEnv *env, jobject instance) { + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + return tx->dust(); +} +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getFee(JNIEnv *env, jobject instance) { + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + return tx->fee(); +} + +/* this returns a vector of strings - deal with this later +JNIEXPORT jstring JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxId(JNIEnv *env, jobject instance) { + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + return env->NewStringUTF(tx->txid().c_str()); +} +*/ + +JNIEXPORT jlong JNICALL +Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxCount(JNIEnv *env, jobject instance) { + Bitmonero::PendingTransaction *tx = getHandle(env, instance); + return tx->txCount(); +} + + #ifdef __cplusplus } #endif diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index d1f846ad..9c5a46a1 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -20,6 +20,7 @@ import android.app.Fragment; import android.content.Context; import android.os.Bundle; import android.text.Editable; +import android.text.InputType; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -66,6 +67,11 @@ public class GenerateFragment extends Fragment { etWalletRestoreHeight = (EditText) view.findViewById(R.id.etWalletRestoreHeight); bGenerate = (Button) view.findViewById(R.id.bGenerate); + etWalletMnemonic.setRawInputType(InputType.TYPE_CLASS_TEXT); + etWalletAddress.setRawInputType(InputType.TYPE_CLASS_TEXT); + etWalletViewKey.setRawInputType(InputType.TYPE_CLASS_TEXT); + etWalletSpendKey.setRawInputType(InputType.TYPE_CLASS_TEXT); + boolean testnet = WalletManager.getInstance().isTestNet(); etWalletMnemonic.setTextIsSelectable(testnet); @@ -318,6 +324,7 @@ public class GenerateFragment extends Fragment { private boolean addressOk() { String address = etWalletAddress.getText().toString(); + // TODO only accept address from the correct net return ((address.length() == 95) && ("49A".indexOf(address.charAt(0)) >= 0)); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index d74b1037..6448b0cc 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -283,6 +283,10 @@ public class LoginActivity extends AppCompatActivity transaction.commit(); } + void popFragmentStack(String name) { + getFragmentManager().popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + ////////////////////////////////////////// // GenerateFragment.Listener ////////////////////////////////////////// @@ -393,8 +397,7 @@ public class LoginActivity extends AppCompatActivity && (testWallet(walletPath, password) == Wallet.Status.Status_Ok); if (rc) { - getFragmentManager().popBackStack("gen", - FragmentManager.POP_BACK_STACK_INCLUSIVE); + popFragmentStack("gen"); Toast.makeText(LoginActivity.this, getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show(); } else { diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java new file mode 100644 index 00000000..422040e3 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.text.InputType; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import com.m2049r.xmrwallet.model.PendingTransaction; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.TxData; + +public class SendFragment extends Fragment { + static final String TAG = "GenerateFragment"; + + static final public String ARG_WALLETID = "walletId"; + + EditText etAddress; + EditText etPaymentId; + EditText etAmount; + Button bSweep; + Spinner sMixin; + Spinner sPriority; + Button bPrepareSend; + LinearLayout llConfirmSend; + TextView tvTxAmount; + TextView tvTxFee; + TextView tvTxDust; + Button bSend; + + final static int Mixins[] = {4, 6, 8, 10, 13}; // must macth the layout + final static PendingTransaction.Priority Priorities[] = + {PendingTransaction.Priority.Priority_Low, + PendingTransaction.Priority.Priority_Medium, + PendingTransaction.Priority.Priority_High}; // must macth the layout + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.send_fragment, container, false); + + sMixin = (Spinner) view.findViewById(R.id.sMixin); + sPriority = (Spinner) view.findViewById(R.id.sPriority); + etAddress = (EditText) view.findViewById(R.id.etAddress); + etPaymentId = (EditText) view.findViewById(R.id.etPaymentId); + etAmount = (EditText) view.findViewById(R.id.etAmount); + bSweep = (Button) view.findViewById(R.id.bSweep); + bPrepareSend = (Button) view.findViewById(R.id.bPrepareSend); + + llConfirmSend = (LinearLayout) view.findViewById(R.id.llConfirmSend); + tvTxAmount = (TextView) view.findViewById(R.id.tvTxAmount); + tvTxFee = (TextView) view.findViewById(R.id.tvTxFee); + tvTxDust = (TextView) view.findViewById(R.id.tvTxDust); + bSend = (Button) view.findViewById(R.id.bSend); + + etAddress.setRawInputType(InputType.TYPE_CLASS_TEXT); + etPaymentId.setRawInputType(InputType.TYPE_CLASS_TEXT); + + etAddress.setText("9tDC52GsMjTNt4dpnRCwAF7ekVBkbkgkXGaMKTcSTpBhGpqkPX56jCNRydLq9oGjbbAQBsZhLfgmTKsntmxRd3TaJFYM2f8"); + boolean testnet = WalletManager.getInstance().isTestNet(); + // TODO die if NOT testnet + Helper.showKeyboard(getActivity()); + etAddress.requestFocus(); + etAddress.setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + Log.d(TAG, actionId + "/" + (event == null ? null : event.toString())); + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (addressOk()) { + etPaymentId.requestFocus(); + } // otherwise ignore + return true; + } + return false; + } + }); + + etPaymentId.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.showKeyboard(getActivity()); + } + }); + etPaymentId.setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (paymentIdOk()) { + etAmount.requestFocus(); + } // otherwise ignore + return true; + } + return false; + } + }); + + etAmount.setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) { + if (amountOk()) { + Helper.hideKeyboard(getActivity()); + } + return true; + } + return false; + } + }); + + bPrepareSend.setEnabled(true); // TODO need clever logic here + bPrepareSend.setOnClickListener(new View.OnClickListener() + + { + @Override + public void onClick(View v) { + Helper.hideKeyboard(getActivity()); + prepareSend(); + } + }); + + + bSend.setOnClickListener(new View.OnClickListener() + + { + @Override + public void onClick(View v) { + send(); + } + }); + return view; + } + + private boolean addressOk() { + String address = etAddress.getText().toString(); + // TODO only accept address from the correct net + return ((address.length() == 95) && ("49A".indexOf(address.charAt(0)) >= 0)); + } + + private boolean amountOk() { + String amount = etAmount.getText().toString(); + // TODO decimal separator + return ((amount.length() > 0) && (amount.matches("^[0-9]+([,.][0-9]+)?$"))); + } + + private boolean paymentIdOk() { + String spendKey = etPaymentId.getText().toString(); + return ((spendKey.length() == 0) || ((spendKey.length() == 64) && (spendKey.matches("^[0-9a-fA-F]+$")))); + } + + private void prepareSend() { + String dst_addr = etAddress.getText().toString(); + String paymentId = etPaymentId.getText().toString(); + long amount = Wallet.getAmountFromString(etAmount.getText().toString()); + int mixin = Mixins[sMixin.getSelectedItemPosition()]; + int priorityIndex = sPriority.getSelectedItemPosition(); + PendingTransaction.Priority priority = Priorities[priorityIndex]; + Log.d(TAG, dst_addr + "/" + paymentId + "/" + amount + "/" + mixin + "/" + priority.toString()); + TxData txData = new TxData( + dst_addr, + paymentId, + amount, + mixin, + priority); + + sMixin.setEnabled(false); + sPriority.setEnabled(false); + etAddress.setEnabled(false); + etPaymentId.setEnabled(false); + etAmount.setEnabled(false); + bSweep.setEnabled(false); + bPrepareSend.setEnabled(false); + + activityCallback.onPrepareSend(txData); + } + private void send() { + activityCallback.onSend(); + } + + SendFragment.Listener activityCallback; + + public interface Listener { + void onPrepareSend(TxData data); + + void onSend(); + + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof SendFragment.Listener) { + this.activityCallback = (SendFragment.Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + public void onCreatedTransaction(PendingTransaction pendingTransaction) { + PendingTransaction.Status status = pendingTransaction.getStatus(); + if (status != PendingTransaction.Status.Status_Ok) { + Log.d(TAG, "Wallet store failed: " + pendingTransaction.getErrorString()); + } + Log.d(TAG, "transaction amount " + pendingTransaction.getAmount()); + Log.d(TAG, "transaction fee " + pendingTransaction.getFee()); + Log.d(TAG, "transaction dust " + pendingTransaction.getDust()); + Log.d(TAG, "transactions " + pendingTransaction.getTxCount()); + + llConfirmSend.setVisibility(View.VISIBLE); + tvTxAmount.setText(Wallet.getDisplayAmount(pendingTransaction.getAmount())); + tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee())); + tvTxDust.setText(Wallet.getDisplayAmount(pendingTransaction.getDust())); + bSend.setEnabled(true); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 0bb2231b..9e6266ba 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -18,6 +18,8 @@ package com.m2049r.xmrwallet; import android.app.Activity; import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -29,11 +31,13 @@ import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.widget.Toast; +import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.service.WalletService; +import com.m2049r.xmrwallet.util.TxData; public class WalletActivity extends AppCompatActivity implements WalletFragment.Listener, - WalletService.Observer { + WalletService.Observer, SendFragment.Listener { private static final String TAG = "WalletActivity"; static final int MIN_DAEMON_VERSION = 65544; @@ -101,7 +105,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. } - Wallet getWallet() { + public Wallet getWallet() { if (mBoundService == null) throw new IllegalStateException("WalletService not bound."); return mBoundService.getWallet(); } @@ -120,7 +124,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. //Log.d(TAG, "setting observer of " + mBoundService); mBoundService.setObserver(WalletActivity.this); updateProgress(); - //TODO show current progress (eg. if the service is already busy saving last wallet) + //TODO show current pbProgress (eg. if the service is already busy saving last wallet) Log.d(TAG, "CONNECTED"); } @@ -231,6 +235,20 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. super.setTitle(title); } + @Override + public void onSendRequest() { + replaceFragment(new SendFragment(), null, null); + } + + @Override + public void forceUpdate() { + try { + onRefreshed(getWallet(), true); + } catch (IllegalStateException ex) { + Log.e(TAG, ex.getLocalizedMessage()); + } + } + /////////////////////////// // WalletService.Observer /////////////////////////// @@ -260,6 +278,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. runOnUiThread(new Runnable() { public void run() { if (success) { + // TODO signal so we can show/enable send button Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_unloaded), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_unload_failed), Toast.LENGTH_LONG).show(); @@ -268,6 +287,32 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. }); } + @Override + public void onCreatedTransaction(final PendingTransaction pendingTransaction) { + // TODO check which fragment is loaded + final SendFragment sendFragment = (SendFragment) + getFragmentManager().findFragmentById(R.id.fragment_container); + runOnUiThread(new Runnable() { + public void run() { + sendFragment.onCreatedTransaction(pendingTransaction); + } + }); + } + + @Override + public void onSentTransaction(final boolean success) { + runOnUiThread(new Runnable() { + public void run() { + if (success) { + Toast.makeText(WalletActivity.this, getString(R.string.status_transaction_sent), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(WalletActivity.this, getString(R.string.status_transaction_failed), Toast.LENGTH_SHORT).show(); + } + popFragmentStack(null); + } + }); + } + @Override public void onProgress(final String text) { //Log.d(TAG, "PROGRESS: " + text); @@ -300,4 +345,52 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. onProgress(mBoundService.getProgressValue()); } } + + /////////////////////////// + // SendFragment.Listener + /////////////////////////// + + @Override + public void onSend() { + if (mIsBound) { // no point in talking to unbound service + Intent intent = new Intent(getApplicationContext(), WalletService.class); + intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_SEND); + startService(intent); + Log.d(TAG, "SEND TX request sent"); + } else { + Log.e(TAG, "Service not bound"); + } + + } + + @Override + public void onPrepareSend(TxData txData) { + if (mIsBound) { // no point in talking to unbound service + Intent intent = new Intent(getApplicationContext(), WalletService.class); + intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_TX); + intent.putExtra(WalletService.REQUEST_CMD_TX_DATA, txData); + startService(intent); + Log.d(TAG, "CREATE TX request sent"); + } else { + Log.e(TAG, "Service not bound"); + } + } + + void popFragmentStack(String name) { + if (name == null) { + getFragmentManager().popBackStack(); + } else { + getFragmentManager().popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE); + } + } + + void replaceFragment(Fragment newFragment, String name, Bundle extras) { + if (extras != null) { + newFragment.setArguments(extras); + } + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.fragment_container, newFragment); + transaction.addToBackStack(name); + transaction.commit(); + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java index c708b3c8..e116c747 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -29,6 +29,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -45,12 +46,28 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O private TransactionInfoAdapter adapter; private NumberFormat formatter = NumberFormat.getInstance(); + TextView tvBalance; + TextView tvUnlockedBalance; + TextView tvBlockHeightProgress; + TextView tvConnectionStatus; + LinearLayout llProgress; + TextView tvProgress; + ProgressBar pbProgress; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.wallet_fragment, container, false); + tvProgress = (TextView) view.findViewById(R.id.tvProgress); + pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); + llProgress = (LinearLayout) view.findViewById(R.id.llProgress); + tvBalance = (TextView) view.findViewById(R.id.tvBalance); + tvUnlockedBalance = (TextView) view.findViewById(R.id.tvUnlockedBalance); + tvBlockHeightProgress = (TextView) view.findViewById(R.id.tvBlockHeightProgress); + tvConnectionStatus = (TextView) view.findViewById(R.id.tvConnectionStatus); + Button bSend = (Button) view.findViewById(R.id.bSend); + RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list); RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL); @@ -59,8 +76,19 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O this.adapter = new TransactionInfoAdapter(this); recyclerView.setAdapter(adapter); + bSend.setOnClickListener(new View.OnClickListener() + + { + @Override + public void onClick(View v) { + activityCallback.onSendRequest(); + } + }); + activityCallback.setTitle(getString(R.string.status_wallet_loading)); + activityCallback.forceUpdate(); + return view; } @@ -106,34 +134,30 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O } public void onProgress(final String text) { - TextView progressText = (TextView) getView().findViewById(R.id.tvProgress); if (text != null) { - progressText.setText(text); + tvProgress.setText(text); showProgress(); //TODO optimize this } else { hideProgress(); - progressText.setText(getString(R.string.status_working)); + tvProgress.setText(getString(R.string.status_working)); onProgress(-1); } } public void onProgress(final int n) { - ProgressBar progress = (ProgressBar) getView().findViewById(R.id.pbProgress); if (n >= 0) { - progress.setIndeterminate(false); - progress.setProgress(n); + pbProgress.setIndeterminate(false); + pbProgress.setProgress(n); } else { - progress.setIndeterminate(true); + pbProgress.setIndeterminate(true); } } public void showProgress() { - LinearLayout llProgress = (LinearLayout) getView().findViewById(R.id.llProgress); llProgress.setVisibility(View.VISIBLE); } public void hideProgress() { - LinearLayout llProgress = (LinearLayout) getView().findViewById(R.id.llProgress); llProgress.setVisibility(View.GONE); } @@ -149,7 +173,6 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O return title; } - private long firstBlock = 0; private String walletTitle = null; @@ -159,12 +182,8 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O walletTitle = setActivityTitle(wallet); onProgress(100); // of loading } - final TextView balanceView = (TextView) getView().findViewById(R.id.tvBalance); - final TextView unlockedView = (TextView) getView().findViewById(R.id.tvUnlockedBalance); - final TextView syncProgressView = (TextView) getView().findViewById(R.id.tvBlockHeightProgress); - final TextView connectionStatusView = (TextView) getView().findViewById(R.id.tvConnectionStatus); - balanceView.setText(Wallet.getDisplayAmount(wallet.getBalance())); - unlockedView.setText(Wallet.getDisplayAmount(wallet.getUnlockedBalance())); + tvBalance.setText(Wallet.getDisplayAmount(wallet.getBalance())); + tvUnlockedBalance.setText(Wallet.getDisplayAmount(wallet.getUnlockedBalance())); String sync = ""; if (!activityCallback.hasBoundService()) throw new IllegalStateException("WalletService not bound."); @@ -186,8 +205,8 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O } } String net = (wallet.isTestNet() ? getString(R.string.connect_testnet) : getString(R.string.connect_mainnet)); - syncProgressView.setText(sync); - connectionStatusView.setText(net + " " + daemonConnected.toString().substring(17)); + tvBlockHeightProgress.setText(sync); + tvConnectionStatus.setText(net + " " + daemonConnected.toString().substring(17)); } Listener activityCallback; @@ -196,12 +215,16 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O public interface Listener { boolean hasBoundService(); + void forceUpdate(); + Wallet.ConnectionStatus getConnectionStatus(); long getDaemonHeight(); //mBoundService.getDaemonHeight(); void setTitle(String title); + void onSendRequest(); + } @Override diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java b/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java new file mode 100644 index 00000000..d9256d13 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/model/PendingTransaction.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.model; + +public class PendingTransaction { + static { + System.loadLibrary("monerujo"); + } + + public long handle; + + PendingTransaction(long handle) { + this.handle = handle; + } + + public enum Status { + Status_Ok, + Status_Error, + Status_Critical + } + + public enum Priority { + Priority_Low(1), + Priority_Medium(2), + Priority_High(3), + Priority_Last(4); // TODO is this true? + + private int value; + + Priority(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Priority fromInteger(int n) { + switch (n) { + case 1: + return Priority_Low; + case 2: + return Priority_Medium; + case 3: + return Priority_High; + } + return null; + } + } + + public Status getStatus() { + return Status.values()[getStatusJ()]; + } + + public native int getStatusJ(); + + public native String getErrorString(); + + // commit transaction or save to file if filename is provided. + public native boolean commit(String filename, boolean overwrite); + + public native long getAmount(); + + public native long getDust(); + + public native long getFee(); + + //public native String getTxId(); + + public native long getTxCount(); + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java index 9c79df61..b7cf0755 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -88,6 +88,7 @@ public class Wallet { public native boolean store(String path); public boolean close() { + disposePendingTransaction(); return WalletManager.getInstance().close(this); } @@ -161,15 +162,40 @@ public class Wallet { //TODO virtual int autoRefreshInterval() const = 0; -//virtual PendingTransaction * createTransaction(const std::string &dst_addr, const std::string &payment_id, -// optional tvAmount, uint32_t mixin_count, -// PendingTransaction::Priority = PendingTransaction::Priority_Low) = 0; + // TODO - good place to keep this ? + private PendingTransaction pendingTransaction = null; + + public PendingTransaction getPendingTransaction() { + return pendingTransaction; + } + + public void disposePendingTransaction() { + if (pendingTransaction != null) { + disposeTransaction(pendingTransaction); + pendingTransaction = null; + } + } + + public PendingTransaction createTransaction(String dst_addr, String payment_id, + long amount, int mixin_count, + PendingTransaction.Priority priority) { + disposePendingTransaction(); + long txHandle = createTransactionJ(dst_addr, payment_id, amount, mixin_count, priority); + pendingTransaction = new PendingTransaction(txHandle); + return pendingTransaction; + } + + private native long createTransactionJ(String dst_addr, String payment_id, + long mount, int mixin_count, + PendingTransaction.Priority priority); //virtual PendingTransaction * createSweepUnmixableTransaction() = 0; //virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0; //virtual bool submitTransaction(const std::string &fileName) = 0; -//virtual void disposeTransaction(PendingTransaction * t) = 0; + + public native void disposeTransaction(PendingTransaction pendingTransaction); + //virtual bool exportKeyImages(const std::string &filename) = 0; //virtual bool importKeyImages(const std::string &filename) = 0; diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java index 83271401..e4544711 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java +++ b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java @@ -28,21 +28,30 @@ import android.os.Process; import android.util.Log; import com.m2049r.xmrwallet.R; +import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.model.TransactionHistory; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletListener; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.TxData; public class WalletService extends Service { final static String TAG = "WalletService"; public static final String REQUEST_WALLET = "wallet"; public static final String REQUEST = "request"; + public static final String REQUEST_CMD_LOAD = "load"; public static final String REQUEST_CMD_LOAD_PW = "walletPassword"; + public static final String REQUEST_CMD_STORE = "store"; + public static final String REQUEST_CMD_TX = "createTX"; + public static final String REQUEST_CMD_TX_DATA = "data"; + + public static final String REQUEST_CMD_SEND = "send"; + public static final int START_SERVICE = 1; public static final int STOP_SERVICE = 2; @@ -194,6 +203,10 @@ public class WalletService extends Service { void onProgress(int n); void onWalletStored(boolean success); + + void onCreatedTransaction(PendingTransaction pendingTransaction); + + void onSentTransaction(boolean success); } String progressText = null; @@ -264,6 +277,33 @@ public class WalletService extends Service { Log.d(TAG, "Wallet store failed: " + myWallet.getErrorString()); } if (observer != null) observer.onWalletStored(rc); + } else if (cmd.equals(REQUEST_CMD_TX)) { + Wallet myWallet = getWallet(); + Log.d(TAG, "creating tx for wallet: " + myWallet.getName()); + TxData txData = extras.getParcelable(REQUEST_CMD_TX_DATA); + PendingTransaction pendingTransaction = myWallet.createTransaction( + txData.dst_addr, txData.paymentId, txData.amount, txData.mixin, txData.priority); + PendingTransaction.Status status = pendingTransaction.getStatus(); + Log.d(TAG, "transaction status " + status); + Log.d(TAG, "transaction amount " + pendingTransaction.getAmount()); + Log.d(TAG, "transaction fee " + pendingTransaction.getFee()); + if (status != PendingTransaction.Status.Status_Ok) { + Log.d(TAG, "Create Transaction failed: " + pendingTransaction.getErrorString()); + } + // TODO myWallet.disposeTransaction(pendingTransaction); later + if (observer != null) observer.onCreatedTransaction(pendingTransaction); + } else if (cmd.equals(REQUEST_CMD_SEND)) { + Wallet myWallet = getWallet(); + Log.d(TAG, "send tx for wallet: " + myWallet.getName()); + PendingTransaction pendingTransaction = myWallet.getPendingTransaction(); + if (pendingTransaction.getStatus() != PendingTransaction.Status.Status_Ok) { + Log.e(TAG, "PendingTransaction is " + pendingTransaction.getStatus()); + myWallet.disposePendingTransaction(); // it's broken anyway + return; + } + boolean success = pendingTransaction.commit("", true); + myWallet.disposePendingTransaction(); + if (observer != null) observer.onSentTransaction(success); } } break; diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/TxData.java b/app/src/main/java/com/m2049r/xmrwallet/util/TxData.java new file mode 100644 index 00000000..453b747b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/TxData.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2017 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.m2049r.xmrwallet.model.PendingTransaction; + +// https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents +public class TxData implements Parcelable { + public TxData(String dst_addr, + String paymentId, + long amount, + int mixin, + PendingTransaction.Priority priority) { + this.dst_addr = dst_addr; + this.paymentId = paymentId; + this.amount = amount; + this.mixin = mixin; + this.priority = priority; + } + + public String dst_addr; + public String paymentId; + public long amount; + public int mixin; + public PendingTransaction.Priority priority; + + // 99.9% of the time you can just ignore this + @Override + public int describeContents() { + return 0; + } + + // write your object's data to the passed-in Parcel + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(dst_addr); + out.writeString(paymentId); + out.writeLong(amount); + out.writeInt(mixin); + out.writeInt(priority.getValue()); + } + + // this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public TxData createFromParcel(Parcel in) { + return new TxData(in); + } + + public TxData[] newArray(int size) { + return new TxData[size]; + } + }; + + // example constructor that takes a Parcel and gives you an object populated with it's values + private TxData(Parcel in) { + dst_addr = in.readString(); + paymentId = in.readString(); + amount = in.readLong(); + mixin = in.readInt(); + priority = PendingTransaction.Priority.fromInteger(in.readInt()); + + } +} diff --git a/app/src/main/res/layout/gen_fragment.xml b/app/src/main/res/layout/gen_fragment.xml index f9e23b82..186e1b43 100644 --- a/app/src/main/res/layout/gen_fragment.xml +++ b/app/src/main/res/layout/gen_fragment.xml @@ -40,7 +40,7 @@ android:layout_height="wrap_content" android:hint="@string/generate_mnemonic_hint" android:imeOptions="actionNext" - android:inputType="text" + android:inputType="textMultiLine" android:textAlignment="center" android:textSize="16sp" /> @@ -50,7 +50,7 @@ android:layout_height="wrap_content" android:hint="@string/generate_address_hint" android:imeOptions="actionNext" - android:inputType="text" + android:inputType="textMultiLine" android:textAlignment="center" android:textSize="16sp" /> @@ -68,7 +68,7 @@ android:layout_height="wrap_content" android:hint="@string/generate_viewkey_hint" android:imeOptions="actionNext" - android:inputType="text" + android:inputType="textMultiLine" android:textAlignment="center" android:textSize="16sp" /> @@ -78,7 +78,7 @@ android:layout_height="wrap_content" android:hint="@string/generate_spendkey_hint" android:imeOptions="actionNext" - android:inputType="text" + android:inputType="textMultiLine" android:textAlignment="center" android:textSize="16sp" /> diff --git a/app/src/main/res/layout/send_fragment.xml b/app/src/main/res/layout/send_fragment.xml new file mode 100644 index 00000000..fcd2d0e7 --- /dev/null +++ b/app/src/main/res/layout/send_fragment.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + +