tx notes added + tx details + tweaks

merge-requests/3/head
m2049r 7 years ago
parent a37ca4c0e5
commit 37414be4bf

@ -774,7 +774,8 @@ Java_com_m2049r_xmrwallet_model_Wallet_createTransactionJ(JNIEnv *env, jobject i
}
JNIEXPORT jlong JNICALL
Java_com_m2049r_xmrwallet_model_Wallet_createSweepUnmixableTransactionJ(JNIEnv *env, jobject instance) {
Java_com_m2049r_xmrwallet_model_Wallet_createSweepUnmixableTransactionJ(JNIEnv *env,
jobject instance) {
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
Bitmonero::PendingTransaction *tx = wallet->createSweepUnmixableTransaction();
return reinterpret_cast<jlong>(tx);
@ -839,12 +840,40 @@ Java_com_m2049r_xmrwallet_model_Wallet_setDefaultMixin(JNIEnv *env, jobject inst
return wallet->setDefaultMixin(mixin);
}
//virtual bool setUserNote(const std::string &txid, const std::string &note) = 0;
//virtual std::string getUserNote(const std::string &txid) const = 0;
JNIEXPORT jboolean JNICALL
Java_com_m2049r_xmrwallet_model_Wallet_setUserNote(JNIEnv *env, jobject instance,
jstring txid, jstring note) {
const char *_txid = env->GetStringUTFChars(txid, JNI_FALSE);
const char *_note = env->GetStringUTFChars(note, JNI_FALSE);
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
bool success = wallet->setUserNote(_txid, _note);
env->ReleaseStringUTFChars(txid, _txid);
env->ReleaseStringUTFChars(note, _note);
return success;
}
JNIEXPORT jstring JNICALL
Java_com_m2049r_xmrwallet_model_Wallet_getUserNote(JNIEnv *env, jobject instance,
jstring txid) {
const char *_txid = env->GetStringUTFChars(txid, JNI_FALSE);
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
std::string note = wallet->getUserNote(_txid);
env->ReleaseStringUTFChars(txid, _txid);
return env->NewStringUTF(note.c_str());
}
JNIEXPORT jstring JNICALL
Java_com_m2049r_xmrwallet_model_Wallet_getTxKey(JNIEnv *env, jobject instance,
jstring txid) {
jstring txid) {
const char *_txid = env->GetStringUTFChars(txid, JNI_FALSE);
@ -883,8 +912,10 @@ jobject newTransferInstance(JNIEnv *env, uint64_t amount, const std::string &add
jobject newTransferList(JNIEnv *env, Bitmonero::TransactionInfo *info) {
const std::vector<Bitmonero::TransactionInfo::Transfer> &transfers = info->transfers();
if (transfers.size()==0) { // don't create empty Lists
return nullptr;
}
// make new ArrayList
jmethodID java_util_ArrayList_ = env->GetMethodID(class_ArrayList, "<init>", "(I)V");
jmethodID java_util_ArrayList_add = env->GetMethodID(class_ArrayList, "add",
"(Ljava/lang/Object;)Z");
@ -932,8 +963,11 @@ jobject cpp2java(JNIEnv *env, std::vector<Bitmonero::TransactionInfo *> vector)
jobject arrayList = env->NewObject(class_ArrayList, java_util_ArrayList_, vector.size());
for (Bitmonero::TransactionInfo *s: vector) {
if (s->fee()>1) {
LOGE("TX %s %" PRIu64 " %" PRIu64, s->hash().c_str(), s->fee(), s->amount());
if (s->fee() > 1) {
LOGE("TX %s %"
PRIu64
" %"
PRIu64, s->hash().c_str(), s->fee(), s->amount());
}
jobject info = newTransactionInfo(env, s);
env->CallBooleanMethod(arrayList, java_util_ArrayList_add, info);
@ -995,13 +1029,20 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_getFee(JNIEnv *env, jobject i
return tx->fee();
}
/* TODO this returns a vector of strings - deal with this later
// TODO this returns a vector of strings - deal with this later - for now return first one
JNIEXPORT jstring JNICALL
Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxId(JNIEnv *env, jobject instance) {
Java_com_m2049r_xmrwallet_model_PendingTransaction_getFirstTxId(JNIEnv *env, jobject instance) {
Bitmonero::PendingTransaction *tx = getHandle<Bitmonero::PendingTransaction>(env, instance);
return env->NewStringUTF(tx->txid().c_str());
std::vector<std::string> txids = tx->txid();
for (std::string &s: txids) {
LOGD("TX %s", s.c_str());
}
return env->NewStringUTF(txids.front().c_str());
}
*/
JNIEXPORT jlong JNICALL
Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxCount(JNIEnv *env, jobject instance) {

@ -27,7 +27,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
@ -274,13 +273,13 @@ public class LoginActivity extends AppCompatActivity
Log.d(TAG, "GenerateReviewFragment placed");
}
void replaceFragment(Fragment newFragment, String name, Bundle extras) {
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
if (extras != null) {
newFragment.setArguments(extras);
}
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(name);
transaction.addToBackStack(stackName);
transaction.commit();
}

@ -17,8 +17,12 @@
package com.m2049r.xmrwallet;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
@ -36,6 +40,8 @@ import android.widget.Spinner;
import android.widget.TextView;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Transfer;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
@ -56,6 +62,7 @@ public class SendFragment extends Fragment {
TextView tvTxAmount;
TextView tvTxFee;
TextView tvTxDust;
EditText etNotes;
Button bSend;
ProgressBar pbProgress;
@ -84,12 +91,14 @@ public class SendFragment extends Fragment {
tvTxAmount = (TextView) view.findViewById(R.id.tvTxAmount);
tvTxFee = (TextView) view.findViewById(R.id.tvTxFee);
tvTxDust = (TextView) view.findViewById(R.id.tvTxDust);
etNotes = (EditText) view.findViewById(R.id.etNotes);
bSend = (Button) view.findViewById(R.id.bSend);
pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress);
etAddress.setRawInputType(InputType.TYPE_CLASS_TEXT);
etPaymentId.setRawInputType(InputType.TYPE_CLASS_TEXT);
etNotes.setRawInputType(InputType.TYPE_CLASS_TEXT);
etAddress.setText("9tDC52GsMjTNt4dpnRCwAF7ekVBkbkgkXGaMKTcSTpBhGpqkPX56jCNRydLq9oGjbbAQBsZhLfgmTKsntmxRd3TaJFYM2f8");
boolean testnet = WalletManager.getInstance().isTestNet();
@ -143,6 +152,8 @@ public class SendFragment extends Fragment {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
if (amountOk()) {
Helper.hideKeyboard(getActivity());
disableEdit();
prepareSend();
}
return true;
}
@ -175,6 +186,7 @@ public class SendFragment extends Fragment {
@Override
public void onClick(View v) {
Helper.hideKeyboard(getActivity());
disableEdit();
prepareSend();
}
});
@ -196,11 +208,22 @@ public class SendFragment extends Fragment {
}
});
bSend.setOnClickListener(new View.OnClickListener()
etNotes.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;
}
});
{
bSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bSend.setEnabled(false);
send();
}
});
@ -248,8 +271,6 @@ public class SendFragment extends Fragment {
amount,
mixin,
priority);
disableEdit();
showProgress();
activityCallback.onPrepareSend(txData);
}
@ -286,8 +307,9 @@ public class SendFragment extends Fragment {
}
private void send() {
disableEdit(); // prevent this being sent more than once
activityCallback.onSend();
etNotes.setEnabled(false);
String notes = etNotes.getText().toString();
activityCallback.onSend(notes);
}
SendFragment.Listener activityCallback;
@ -297,7 +319,7 @@ public class SendFragment extends Fragment {
void onPrepareSweep();
void onSend();
void onSend(String notes);
String generatePaymentId();
@ -331,6 +353,21 @@ public class SendFragment extends Fragment {
bSend.setEnabled(true);
}
public void onCreatedTransactionFailed(String errorText) {
hideProgress();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getString(R.string.send_error_title));
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
enableEdit();
}
});
builder.setMessage(errorText);
builder.create().show();
}
public void showProgress() {
pbProgress.setIndeterminate(true);
pbProgress.setVisibility(View.VISIBLE);

@ -0,0 +1,236 @@
/*
* 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.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Transfer;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.MoneroHandlerThread;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
public class TxFragment extends Fragment {
static final String TAG = "TxFragment";
static public final String ARG_INFO = "info";
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public TxFragment() {
super();
Calendar cal = Calendar.getInstance();
TimeZone tz = cal.getTimeZone(); //get the local time zone.
TS_FORMATTER.setTimeZone(tz);
}
TextView tvTxTimestamp;
TextView tvTxId;
TextView tvTxKey;
TextView tvTxPaymentId;
TextView tvTxBlockheight;
TextView tvTxAmount;
TextView tvTxFee;
TextView tvTxTransfers;
TextView etTxNotes;
Button bCopy;
Button bTxNotes;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.tx_fragment, container, false);
tvTxTimestamp = (TextView) view.findViewById(R.id.tvTxTimestamp);
tvTxId = (TextView) view.findViewById(R.id.tvTxId);
tvTxKey = (TextView) view.findViewById(R.id.tvTxKey);
tvTxPaymentId = (TextView) view.findViewById(R.id.tvTxPaymentId);
tvTxBlockheight = (TextView) view.findViewById(R.id.tvTxBlockheight);
tvTxAmount = (TextView) view.findViewById(R.id.tvTxAmount);
tvTxFee = (TextView) view.findViewById(R.id.tvTxFee);
tvTxTransfers = (TextView) view.findViewById(R.id.tvTxTransfers);
etTxNotes = (TextView) view.findViewById(R.id.etTxNotes);
bCopy = (Button) view.findViewById(R.id.bCopy);
bTxNotes = (Button) view.findViewById(R.id.bTxNotes);
etTxNotes.setRawInputType(InputType.TYPE_CLASS_TEXT);
bCopy.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
copyToClipboard();
}
});
bTxNotes.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
info.notes = null; // force reload on next view
bTxNotes.setEnabled(false);
etTxNotes.setEnabled(false);
activityCallback.onSetNote(info.hash, etTxNotes.getText().toString());
}
});
Bundle args = getArguments();
TransactionInfo info = args.getParcelable(ARG_INFO);
show(info);
return view;
}
public void onNotesSet(boolean reload) {
bTxNotes.setEnabled(true);
etTxNotes.setEnabled(true);
if (reload) {
loadNotes(this.info);
}
}
void copyToClipboard() {
if (this.info == null) return;
StringBuffer sb = new StringBuffer();
sb.append(getString(R.string.tx_address)).append(": ");
sb.append(activityCallback.getWalletAddress()).append("\n");
sb.append(getString(R.string.tx_id)).append(": ");
sb.append(info.hash).append("\n");
sb.append(getString(R.string.tx_key)).append(": ");
sb.append(info.txKey.isEmpty() ? "-" : info.txKey).append("\n");
sb.append(getString(R.string.tx_paymentId)).append(": ");
sb.append(info.paymentId).append("\n");
sb.append(getString(R.string.tx_amount)).append(": ");
sb.append(Wallet.getDisplayAmount(info.amount)).append("\n");
sb.append(getString(R.string.tx_fee)).append(": ");
sb.append(Wallet.getDisplayAmount(info.fee)).append("\n");
sb.append(getString(R.string.tx_notes)).append(": ");
String oneLineNotes = info.notes.replace("\n", " ; ");
sb.append(oneLineNotes.isEmpty() ? "-" : oneLineNotes).append("\n");
sb.append(getString(R.string.tx_timestamp)).append(": ");
sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n");
sb.append(getString(R.string.tx_blockheight)).append(": ");
sb.append(info.blockheight).append("\n");
sb.append(getString(R.string.tx_transfers)).append(": ");
if (info.transfers != null) {
boolean comma = false;
for (int i = 0; i < 10; i++)
for (Transfer transfer : info.transfers) {
if (comma) {
sb.append(",");
} else {
comma = true;
}
sb.append("[").append(transfer.address.substring(0, 6)).append("] ");
sb.append(Wallet.getDisplayAmount(transfer.amount));
}
} else {
sb.append("-");
}
sb.append("\n");
ClipboardManager clipboardManager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.tx_copy_label), sb.toString());
clipboardManager.setPrimaryClip(clip);
Toast.makeText(getActivity(), getString(R.string.tx_copy_message), Toast.LENGTH_SHORT).show();
Log.d(TAG, sb.toString());
}
TransactionInfo info = null;
void loadNotes(TransactionInfo info) {
if (info.notes == null) {
info.notes = activityCallback.getTxNotes(info.hash);
//Log.d(TAG, "NOTES:" + info.notes + ":");
}
etTxNotes.setText(info.notes);
}
private void show(TransactionInfo info) {
if (info.txKey == null) {
info.txKey = activityCallback.getTxKey(info.hash);
//Log.d(TAG, "TXKEY:" + info.txKey + ":");
}
loadNotes(info);
tvTxTimestamp.setText(TS_FORMATTER.format(new Date(info.timestamp * 1000)));
tvTxId.setText(info.hash);
tvTxKey.setText(info.txKey.isEmpty() ? "-" : info.txKey);
tvTxPaymentId.setText(info.paymentId);
tvTxBlockheight.setText("" + info.blockheight);
tvTxAmount.setText(Wallet.getDisplayAmount(info.amount));
tvTxFee.setText(Wallet.getDisplayAmount(info.fee));
StringBuffer sb = new StringBuffer();
if (info.transfers != null) {
boolean newline = false;
for (int i = 0; i < 10; i++)
for (Transfer transfer : info.transfers) {
if (newline) {
sb.append("\n");
} else {
newline = true;
}
sb.append("[").append(transfer.address.substring(0, 6)).append("] ");
sb.append(Wallet.getDisplayAmount(transfer.amount));
}
} else {
sb.append("-");
}
tvTxTransfers.setText(sb.toString());
this.info = info;
bCopy.setEnabled(true);
}
TxFragment.Listener activityCallback;
public interface Listener {
String getWalletAddress();
String getTxKey(String hash);
String getTxNotes(String hash);
void onSetNote(String txId, String notes);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof TxFragment.Listener) {
this.activityCallback = (TxFragment.Listener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
}

@ -32,12 +32,13 @@ import android.util.Log;
import android.widget.Toast;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.TransactionInfo;
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, SendFragment.Listener {
WalletService.Observer, SendFragment.Listener, TxFragment.Listener {
private static final String TAG = "WalletActivity";
static final int MIN_DAEMON_VERSION = 65544;
@ -62,6 +63,11 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
return getWallet().getTxKey(txId);
}
@Override
public String getTxNotes(String txId) {
return getWallet().getUserNote(txId);
}
@Override
protected void onStart() {
super.onStart();
@ -258,6 +264,13 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
replaceFragment(new SendFragment(), null, null);
}
@Override
public void onTxDetailsRequest(TransactionInfo info) {
Bundle args = new Bundle();
args.putParcelable(TxFragment.ARG_INFO, info);
replaceFragment(new TxFragment(), null, args);
}
@Override
public void forceUpdate() {
try {
@ -299,6 +312,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
return true;
} catch (ClassCastException ex) {
// not in wallet fragment (probably send monero)
Log.d(TAG, ex.getLocalizedMessage());
// keep calm and carry on
}
return false;
@ -319,18 +333,16 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
@Override
public void onCreatedTransaction(final PendingTransaction pendingTransaction) {
final PendingTransaction.Status status = pendingTransaction.getStatus();
if (status != PendingTransaction.Status.Status_Ok) {
getWallet().disposePendingTransaction();
}
try {
final SendFragment sendFragment = (SendFragment)
getFragmentManager().findFragmentById(R.id.fragment_container);
runOnUiThread(new Runnable() {
public void run() {
PendingTransaction.Status status = pendingTransaction.getStatus();
if (status != PendingTransaction.Status.Status_Ok) {
Toast.makeText(WalletActivity.this, getString(R.string.status_transaction_prepare_failed), Toast.LENGTH_LONG).show();
sendFragment.onCreatedTransaction(null);
String errorText = pendingTransaction.getErrorString();
getWallet().disposePendingTransaction();
sendFragment.onCreatedTransactionFailed(errorText);
} else {
sendFragment.onCreatedTransaction(pendingTransaction);
}
@ -338,6 +350,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
});
} catch (ClassCastException ex) {
// not in spend fragment
Log.d(TAG, ex.getLocalizedMessage());
// don't need the transaction any more
getWallet().disposePendingTransaction();
}
@ -357,6 +370,26 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
});
}
@Override
public void onSetNotes(final boolean success) {
try {
final TxFragment txFragment = (TxFragment)
getFragmentManager().findFragmentById(R.id.fragment_container);
runOnUiThread(new Runnable() {
public void run() {
if (!success) {
Toast.makeText(WalletActivity.this, getString(R.string.tx_notes_set_failed), Toast.LENGTH_LONG).show();
}
txFragment.onNotesSet(success);
}
});
} catch (ClassCastException ex) {
// not in tx fragment
Log.d(TAG, ex.getLocalizedMessage());
// never min
}
}
@Override
public void onProgress(final String text) {
try {
@ -369,6 +402,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
});
} catch (ClassCastException ex) {
// not in wallet fragment (probably send monero)
Log.d(TAG, ex.getLocalizedMessage());
// keep calm and carry on
}
}
@ -385,6 +419,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
});
} catch (ClassCastException ex) {
// not in wallet fragment (probably send monero)
Log.d(TAG, ex.getLocalizedMessage());
// keep calm and carry on
}
}
@ -402,10 +437,11 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
///////////////////////////
@Override
public void onSend() {
public void onSend(String notes) {
if (mIsBound) { // no point in talking to unbound service
Intent intent = new Intent(getApplicationContext(), WalletService.class);
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_SEND);
intent.putExtra(WalletService.REQUEST_CMD_SEND_NOTES, notes);
startService(intent);
Log.d(TAG, "SEND TX request sent");
} else {
@ -414,6 +450,21 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
}
@Override
public void onSetNote(String txId, String notes) {
if (mIsBound) { // no point in talking to unbound service
Intent intent = new Intent(getApplicationContext(), WalletService.class);
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_SETNOTE);
intent.putExtra(WalletService.REQUEST_CMD_SETNOTE_TX, txId);
intent.putExtra(WalletService.REQUEST_CMD_SETNOTE_NOTES, notes);
startService(intent);
Log.d(TAG, "SET NOTE request sent");
} else {
Log.e(TAG, "Service not bound");
}
}
@Override
public void onPrepareSend(TxData txData) {
if (mIsBound) { // no point in talking to unbound service
@ -462,13 +513,13 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
}
}
void replaceFragment(Fragment newFragment, String name, Bundle extras) {
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
if (extras != null) {
newFragment.setArguments(extras);
}
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(name);
transaction.addToBackStack(stackName);
transaction.commit();
}
}

@ -102,61 +102,7 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O
// Callbacks from TransactionInfoAdapter
@Override
public void onInteraction(final View view, final TransactionInfo infoItem) {
final Context ctx = view.getContext();
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setTitle("Transaction details");
if (infoItem.txKey == null) {
infoItem.txKey = activityCallback.getTxKey(infoItem.hash);
}
builder.setPositiveButton("Copy TX ID", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ClipboardManager clipboardManager = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("TXID", infoItem.hash);
clipboardManager.setPrimaryClip(clip);
}
});
if (!infoItem.txKey.isEmpty()) {
builder.setNegativeButton("Copy TX Key", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ClipboardManager clipboardManager = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("TXKEY", infoItem.txKey);
clipboardManager.setPrimaryClip(clip);
}
});
}
// TODO use strings.xml
StringBuffer sb = new StringBuffer();
sb.append("TX ID: ").append(infoItem.hash);
sb.append("\nTX Key: ");
if (!infoItem.txKey.isEmpty()) {
sb.append(infoItem.txKey);
} else {
sb.append(" -");
}
sb.append("\nPayment ID: ").append(infoItem.paymentId);
sb.append("\nBlockHeight: ").append(infoItem.blockheight);
sb.append("\nAmount: ");
sb.append(infoItem.direction == TransactionInfo.Direction.Direction_In ? "+" : "-");
sb.append(Wallet.getDisplayAmount(infoItem.amount));
sb.append("\nFee: ").append(Wallet.getDisplayAmount(infoItem.fee));
sb.append("\nTransfers:");
if (infoItem.transfers.size() > 0) {
for (Transfer transfer : infoItem.transfers) {
sb.append("\n[").append(transfer.address.substring(0, 6)).append("] ");
sb.append(Wallet.getDisplayAmount(transfer.amount));
}
} else {
sb.append(" -");
}
builder.setMessage(sb.toString());
AlertDialog alert1 = builder.create();
alert1.show();
activityCallback.onTxDetailsRequest(infoItem);
}
// called from activity
@ -211,7 +157,9 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O
if (shortName.length() > 16) {
shortName = shortName.substring(0, 14) + "...";
}
String title = (wallet.isWatchOnly() ? "X " : "") + "[" + wallet.getAddress().substring(0, 6) + "] " + shortName;
String title = "[" + wallet.getAddress().substring(0, 6) + "] "
+ shortName
+ (wallet.isWatchOnly() ? " " + getString(R.string.watchonly_label) : "");
activityCallback.setTitle(title);
Log.d(TAG, "wallet title is " + title);
return title;
@ -269,6 +217,8 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O
void onSendRequest();
void onTxDetailsRequest(TransactionInfo info);
boolean isSynced();
boolean isWatchOnly();

@ -40,8 +40,8 @@ import java.util.TimeZone;
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
private static final String TAG = "TransactionInfoAdapter";
private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd");
private static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("HH:mm:ss");
private final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd");
private final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("HH:mm:ss");
static final int TX_RED = Color.rgb(255, 79, 65);
static final int TX_GREEN = Color.rgb(54, 176, 91);

@ -39,16 +39,6 @@ public class PendingTransaction {
Priority_High(3),
Priority_Last(4);
private int value;
Priority(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static Priority fromInteger(int n) {
switch (n) {
case 1:
@ -60,6 +50,18 @@ public class PendingTransaction {
}
return null;
}
public int getValue() {
return value;
}
private int value;
Priority(int value) {
this.value = value;
}
}
public Status getStatus() {
@ -79,7 +81,7 @@ public class PendingTransaction {
public native long getFee();
//public native String getTxId();
public native String getFirstTxId();
public native long getTxCount();

@ -16,16 +16,40 @@
package com.m2049r.xmrwallet.model;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.util.List;
// this is not the TransactionInfo from the API as that is owned by the TransactionHistory
// this is a POJO for the TransactionInfoAdapter
public class TransactionInfo {
public class TransactionInfo implements Parcelable {
static final String TAG = "TransactionInfo";
public enum Direction {
Direction_In,
Direction_Out
Direction_In(0),
Direction_Out(1);
public static Direction fromInteger(int n) {
switch (n) {
case 0:
return Direction_In;
case 1:
return Direction_Out;
}
return null;
}
public int getValue() {
return value;
}
private int value;
Direction(int value) {
this.value = value;
}
}
public Direction direction;
@ -41,6 +65,7 @@ public class TransactionInfo {
public List<Transfer> transfers;
public String txKey = null;
public String notes = null;
public TransactionInfo(
int direction,
@ -71,4 +96,52 @@ public class TransactionInfo {
return direction + "@" + blockheight + " " + amount;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(direction.getValue());
out.writeByte((byte) (isPending ? 1 : 0));
out.writeByte((byte) (isFailed ? 1 : 0));
out.writeLong(amount);
out.writeLong(fee);
out.writeLong(blockheight);
out.writeString(hash);
out.writeLong(timestamp);
out.writeString(paymentId);
out.writeLong(confirmations);
out.writeList(transfers);
out.writeString(txKey);
out.writeString(notes);
}
public static final Parcelable.Creator<TransactionInfo> CREATOR = new Parcelable.Creator<TransactionInfo>() {
public TransactionInfo createFromParcel(Parcel in) {
return new TransactionInfo(in);
}
public TransactionInfo[] newArray(int size) {
return new TransactionInfo[size];
}
};
private TransactionInfo(Parcel in) {
direction = Direction.fromInteger(in.readInt());
isPending = in.readByte() != 0;
isFailed = in.readByte() != 0;
amount = in.readLong();
fee = in.readLong();
blockheight = in.readLong();
hash = in.readString();
timestamp = in.readLong();
paymentId = in.readString();
confirmations = in.readLong();
transfers = in.readArrayList(Transfer.class.getClassLoader());
txKey = in.readString();
notes = in.readString();
}
@Override
public int describeContents() {
return 0;
}
}

@ -16,9 +16,12 @@
package com.m2049r.xmrwallet.model;
import android.util.Log;
import android.os.Parcel;
import android.os.Parcelable;
public class Transfer {
import com.m2049r.xmrwallet.util.TxData;
public class Transfer implements Parcelable {
public long amount;
public String address;
@ -26,4 +29,31 @@ public class Transfer {
this.amount = amount;
this.address = address;
}
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeLong(amount);
out.writeString(address);
}
public static final Parcelable.Creator<Transfer> CREATOR = new Parcelable.Creator<Transfer>() {
public Transfer createFromParcel(Parcel in) {
return new Transfer(in);
}
public Transfer[] newArray(int size) {
return new Transfer[size];
}
};
private Transfer(Parcel in) {
amount = in.readLong();
address = in.readString();
}
@Override
public int describeContents() {
return 0;
}
}

@ -233,8 +233,9 @@ public class Wallet {
public native void setDefaultMixin(int mixin);
//virtual bool setUserNote(const std::string &txid, const std::string &note) = 0;
//virtual std::string getUserNote(const std::string &txid) const = 0;
public native boolean setUserNote(String txid, String note);
public native String getUserNote(String txid);
public native String getTxKey(String txid);
//virtual std::string signMessage(const std::string &message) = 0;

@ -53,6 +53,11 @@ public class WalletService extends Service {
public static final String REQUEST_CMD_SWEEP = "sweepTX";
public static final String REQUEST_CMD_SEND = "send";
public static final String REQUEST_CMD_SEND_NOTES = "notes";
public static final String REQUEST_CMD_SETNOTE = "setnote";
public static final String REQUEST_CMD_SETNOTE_TX = "tx";
public static final String REQUEST_CMD_SETNOTE_NOTES = "notes";
public static final int START_SERVICE = 1;
public static final int STOP_SERVICE = 2;
@ -211,6 +216,8 @@ public class WalletService extends Service {
void onCreatedTransaction(PendingTransaction pendingTransaction);
void onSentTransaction(boolean success);
void onSetNotes(boolean success);
}
String progressText = null;
@ -320,11 +327,15 @@ public class WalletService extends Service {
myWallet.disposePendingTransaction(); // it's broken anyway
return;
}
String txid = pendingTransaction.getFirstTxId();
boolean success = pendingTransaction.commit("", true);
myWallet.disposePendingTransaction();
if (observer != null) observer.onSentTransaction(success);
if (success) {
String notes = extras.getString(REQUEST_CMD_SEND_NOTES);
if ((notes != null) && (!notes.isEmpty())) {
myWallet.setUserNote(txid, notes);
}
boolean rc = myWallet.store();
Log.d(TAG, "wallet stored: " + myWallet.getName() + " with rc=" + rc);
if (!rc) {
@ -332,6 +343,26 @@ public class WalletService extends Service {
}
if (observer != null) observer.onWalletStored(rc);
}
} else if (cmd.equals(REQUEST_CMD_SETNOTE)) {
Wallet myWallet = getWallet();
Log.d(TAG, "SET NOTE for wallet: " + myWallet.getName());
String txId = extras.getString(REQUEST_CMD_SETNOTE_TX);
String notes = extras.getString(REQUEST_CMD_SETNOTE_NOTES);
if ((txId != null) && (notes != null)) {
boolean success = myWallet.setUserNote(txId, notes);
if (!success) {
Log.e(TAG, myWallet.getErrorString());
}
if (observer != null) observer.onSetNotes(success);
if (success) {
boolean rc = myWallet.store();
Log.d(TAG, "wallet stored: " + myWallet.getName() + " with rc=" + rc);
if (!rc) {
Log.d(TAG, "Wallet store failed: " + myWallet.getErrorString());
}
if (observer != null) observer.onWalletStored(rc);
}
}
}
}
break;

@ -24,10 +24,10 @@ 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) {
String paymentId,
long amount,
int mixin,
PendingTransaction.Priority priority) {
this.dst_addr = dst_addr;
this.paymentId = paymentId;
this.amount = amount;
@ -41,13 +41,6 @@ public class TxData implements Parcelable {
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);
@ -68,7 +61,6 @@ public class TxData implements Parcelable {
}
};
// 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();
@ -77,4 +69,10 @@ public class TxData implements Parcelable {
priority = PendingTransaction.Priority.fromInteger(in.readInt());
}
@Override
public int describeContents() {
return 0;
}
}

@ -5,7 +5,7 @@
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">

@ -5,7 +5,7 @@
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">
@ -32,7 +32,7 @@
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
@ -59,7 +59,7 @@
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="vertical">
@ -67,7 +67,7 @@
<ProgressBar
android:id="@+id/pbProgress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible" />
</LinearLayout>
@ -103,10 +103,10 @@
android:id="@+id/tvWalletAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:selectAllOnFocus="true"
android:textAlignment="center"
android:textColor="@color/colorPrimaryDark"
android:textIsSelectable="true"
android:selectAllOnFocus="true"
android:textSize="16sp" />
<TextView
@ -123,10 +123,10 @@
android:id="@+id/tvWalletViewKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:selectAllOnFocus="true"
android:textAlignment="center"
android:textColor="@color/colorPrimaryDark"
android:textIsSelectable="true"
android:selectAllOnFocus="true"
android:textSize="16sp" />
<TextView
@ -143,10 +143,10 @@
android:id="@+id/tvWalletSpendKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:selectAllOnFocus="true"
android:textAlignment="center"
android:textColor="@color/colorPrimaryDark"
android:textIsSelectable="true"
android:selectAllOnFocus="true"
android:textSize="16sp" />
<Button
@ -160,14 +160,14 @@
<LinearLayout
android:id="@+id/llFunctions"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="vertical"
android:visibility="gone">
<View
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="2dip"
android:background="@color/colorPrimary" />

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_root"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp" >

@ -5,7 +5,7 @@
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">
@ -39,7 +39,7 @@
android:textSize="16sp" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="10">
@ -69,7 +69,7 @@
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="10">
@ -94,6 +94,7 @@
android:layout_weight="2"
android:background="@color/colorPrimary"
android:enabled="true"
android:visibility="invisible"
android:text="@string/send_sweep_hint" />
</LinearLayout>
@ -110,7 +111,7 @@
<ProgressBar
android:id="@+id/pbProgress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_height="wrap_content" />
@ -122,7 +123,7 @@
android:visibility="gone">
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="3">
@ -149,7 +150,7 @@
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="3">
@ -176,7 +177,7 @@
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="3">
@ -191,7 +192,6 @@
android:textAlignment="textEnd"
android:textColor="@color/colorAccent"
android:textSize="20sp" />
c
<TextView
android:id="@+id/tvTxDust"
@ -203,6 +203,17 @@
android:textSize="20sp" />
</LinearLayout>
<EditText
android:id="@+id/etNotes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/send_notes_hint"
android:imeOptions="actionDone"
android:inputType="textMultiLine"
android:textAlignment="center"
android:textSize="16sp" />
<Button
android:id="@+id/bSend"
android:layout_width="match_parent"
@ -212,6 +223,7 @@
android:background="@color/colorPrimary"
android:enabled="false"
android:text="@string/send_send_hint" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:shrinkColumns="1">
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_id"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tvTxId"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_key"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tvTxKey"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_paymentId"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tvTxPaymentId"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:selectAllOnFocus="true"
android:text="@string/tx_amount"
android:textColor="@color/colorAccent"
android:textIsSelectable="true" />
<TextView
android:id="@+id/tvTxAmount"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_fee"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tvTxFee"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_notes"
android:textColor="@color/colorAccent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="10">
<EditText
android:id="@+id/etTxNotes"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="8"
android:hint="@string/tx_notes_hint"
android:inputType="textMultiLine"
android:textAlignment="textStart"
android:textSize="14sp" />
<Button
android:id="@+id/bTxNotes"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:layout_weight="2"
android:background="@color/colorPrimary"
android:enabled="true"
android:text="@string/tx_button_notes"
android:textSize="8sp" />
</LinearLayout>
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_timestamp"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tvTxTimestamp"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_blockheight"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tvTxBlockheight"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
<TableRow>
<TextView
android:gravity="right"
android:padding="8dp"
android:text="@string/tx_transfers"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tvTxTransfers"
android:gravity="left"
android:padding="8dip"
android:selectAllOnFocus="true"
android:textIsSelectable="true" />
</TableRow>
</TableLayout>
<Button
android:id="@+id/bCopy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@color/colorPrimary"
android:enabled="false"
android:text="@string/tx_button_copy" />
</LinearLayout>
</ScrollView>

@ -96,7 +96,7 @@
<ProgressBar
android:id="@+id/pbProgress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progress="0" />
</LinearLayout>

@ -28,6 +28,8 @@
<string name="warn_daemon_unavailable">Cannot connect to daemon! Try again.</string>
<string name="panic">Something\'s wrong!</string>
<string name="watchonly_label">(Watch Only)</string>
<string name="wallet_send_hint">Send some</string>
<string name="title_amount">Amount</string>
@ -105,7 +107,7 @@
<string name="send_sweep_hint">Sweep</string>
<string name="send_generate_paymentid_hint">Generate</string>
<string name="send_prepare_hint">Prepare</string>
<string name="send_send_hint">Get rid of my Monero!</string>
<string name="send_send_hint">Spend my sweet Moneroj</string>
<string name="send_preparing_progress">Preparing transaction</string>
@ -113,6 +115,26 @@
<string name="send_fee_label">Fee</string>
<string name="send_dust_label">Dust</string>
<string name="send_error_title">Trasaction Error</string>
<string name="tx_address">Address</string>
<string name="tx_timestamp">Timestamp</string>
<string name="tx_id">TX ID</string>
<string name="tx_key">TX Key</string>
<string name="tx_paymentId">Payment ID</string>
<string name="tx_blockheight">Block</string>
<string name="tx_amount">Amount</string>
<string name="tx_fee">Fee</string>
<string name="tx_transfers">Transfers</string>
<string name="tx_notes">Notes</string>
<string name="tx_button_copy">Copy to Clipboard</string>
<string name="tx_copy_message">Copied to Clipboard</string>
<string name="tx_copy_label">Transaction Details</string>
<string name="tx_notes_hint">(optional)</string>
<string name="tx_button_notes">Save</string>
<string name="tx_notes_set">Notes saved</string>
<string name="tx_notes_set_failed">Notes saved failed</string>
<string name="big_amount">999999.999999999999</string>
<string-array name="mixin">

Loading…
Cancel
Save