From cf4ff856d5471d2c42d5eab056133ea2baa7464f Mon Sep 17 00:00:00 2001 From: m2049r Date: Mon, 22 Mar 2021 00:31:01 +0100 Subject: [PATCH] Subaddresses as first class citizens (#733) --- app/build.gradle | 4 +- .../com/m2049r/xmrwallet/BaseActivity.java | 6 +- .../xmrwallet/GenerateReviewFragment.java | 1 - .../com/m2049r/xmrwallet/LoginActivity.java | 40 +-- .../com/m2049r/xmrwallet/LoginFragment.java | 9 - .../com/m2049r/xmrwallet/NodeFragment.java | 15 +- .../com/m2049r/xmrwallet/ReceiveFragment.java | 199 +++----------- .../m2049r/xmrwallet/SubaddressFragment.java | 246 ++++++++++++++++++ .../xmrwallet/SubaddressInfoFragment.java | 170 ++++++++++++ .../java/com/m2049r/xmrwallet/TxFragment.java | 74 ++++-- .../com/m2049r/xmrwallet/WalletActivity.java | 72 ++++- .../com/m2049r/xmrwallet/WalletFragment.java | 45 ++-- .../com/m2049r/xmrwallet/data/Subaddress.java | 59 +++++ .../xmrwallet/fragment/send/SendFragment.java | 2 +- .../xmrwallet/layout/NodeInfoAdapter.java | 6 +- .../layout/SubaddressInfoAdapter.java | 155 +++++++++++ .../layout/TransactionInfoAdapter.java | 27 +- .../xmrwallet/model/TransactionHistory.java | 10 +- .../xmrwallet/model/TransactionInfo.java | 46 ++-- .../com/m2049r/xmrwallet/model/Wallet.java | 27 +- app/src/main/res/layout/fragment_receive.xml | 50 ++-- .../main/res/layout/fragment_subaddress.xml | 74 ++++++ .../res/layout/fragment_subaddressinfo.xml | 56 ++++ app/src/main/res/layout/fragment_tx_info.xml | 5 +- app/src/main/res/layout/item_subaddress.xml | 32 +++ app/src/main/res/menu/list_context_menu.xml | 5 - app/src/main/res/menu/wallet_menu.xml | 6 + app/src/main/res/values-cat/strings.xml | 16 +- app/src/main/res/values-de/strings.xml | 16 +- app/src/main/res/values-el/strings.xml | 16 +- app/src/main/res/values-eo/strings.xml | 16 +- app/src/main/res/values-es/strings.xml | 18 +- app/src/main/res/values-et/strings.xml | 16 +- app/src/main/res/values-fr/strings.xml | 16 +- app/src/main/res/values-hu/strings.xml | 16 +- app/src/main/res/values-it/strings.xml | 16 +- app/src/main/res/values-ja/strings.xml | 16 +- app/src/main/res/values-nb/strings.xml | 16 +- app/src/main/res/values-nl/strings.xml | 16 +- app/src/main/res/values-pt-rBR/strings.xml | 16 +- app/src/main/res/values-pt/strings.xml | 16 +- app/src/main/res/values-ro/strings.xml | 16 +- app/src/main/res/values-ru/strings.xml | 16 +- app/src/main/res/values-sk/strings.xml | 16 +- app/src/main/res/values-sr/strings.xml | 16 +- app/src/main/res/values-sv/strings.xml | 16 +- app/src/main/res/values-uk/strings.xml | 16 +- app/src/main/res/values-zh-rCN/strings.xml | 16 +- app/src/main/res/values-zh-rTW/strings.xml | 16 +- app/src/main/res/values/strings.xml | 30 ++- app/src/main/res/values/styles.xml | 5 + build.gradle | 2 +- 52 files changed, 1399 insertions(+), 433 deletions(-) create mode 100644 app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java create mode 100644 app/src/main/java/com/m2049r/xmrwallet/layout/SubaddressInfoAdapter.java create mode 100644 app/src/main/res/layout/fragment_subaddress.xml create mode 100644 app/src/main/res/layout/fragment_subaddressinfo.xml create mode 100644 app/src/main/res/layout/item_subaddress.xml diff --git a/app/build.gradle b/app/build.gradle index e113f720..4aa65ad0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 targetSdkVersion 29 - versionCode 713 - versionName "1.17.13 'Druk'" + versionCode 800 + versionName "1.18.0 'ChAdOx1'" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java b/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java index d30f2978..b72c35bc 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/BaseActivity.java @@ -31,10 +31,11 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; +import android.widget.Toast; + import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; -import android.widget.Toast; import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.dialog.ProgressDialog; @@ -46,7 +47,8 @@ import java.io.IOException; import timber.log.Timber; -public class BaseActivity extends SecureActivity implements GenerateReviewFragment.ProgressListener { +public class BaseActivity extends SecureActivity + implements GenerateReviewFragment.ProgressListener, SubaddressFragment.ProgressListener { ProgressDialog progressDialog = null; diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index 310e3189..863ad00a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -307,7 +307,6 @@ public class GenerateReviewFragment extends Fragment { void dismissProgressDialog(); } - public interface AcceptListener { void onAccept(String name, String password); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index a328bf8a..be6dd760 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -59,13 +59,13 @@ import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.service.WalletService; -import com.m2049r.xmrwallet.util.ThemeHelper; import com.m2049r.xmrwallet.util.DayNightMode; import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.KeyStoreHelper; import com.m2049r.xmrwallet.util.LocaleHelper; import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; import com.m2049r.xmrwallet.util.NightmodeHelper; +import com.m2049r.xmrwallet.util.ThemeHelper; import com.m2049r.xmrwallet.widget.Toolbar; import java.io.File; @@ -86,8 +86,7 @@ import timber.log.Timber; public class LoginActivity extends BaseActivity implements LoginFragment.Listener, GenerateFragment.Listener, - GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, - ReceiveFragment.Listener, NodeFragment.Listener { + GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, NodeFragment.Listener { private static final String GENERATE_STACK = "gen"; private static final String NODES_PREFS_NAME = "nodes"; @@ -397,28 +396,6 @@ public class LoginActivity extends BaseActivity .show(); } - @Override - public void onWalletReceive(String walletName) { - Timber.d("receive for wallet .%s.", walletName); - if (checkServiceRunning()) return; - final File walletFile = Helper.getWalletFile(this, walletName); - if (WalletManager.getInstance().walletExists(walletFile)) { - Helper.promptPassword(LoginActivity.this, walletName, false, new Helper.PasswordAction() { - @Override - public void act(String walletName, String password, boolean fingerprintUsed) { - if (checkDevice(walletName, password)) - startReceive(walletFile, password); - } - - @Override - public void fail(String walletName) { - } - }); - } else { // this cannot really happen as we prefilter choices - Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); - } - } - private class AsyncRename extends AsyncTask { @Override protected void onPreExecute() { @@ -797,14 +774,6 @@ public class LoginActivity extends BaseActivity startReviewFragment(b); } - void startReceive(File walletFile, String password) { - Timber.d("startReceive()"); - Bundle b = new Bundle(); - b.putString("path", walletFile.getAbsolutePath()); - b.putString("password", password); - startReceiveFragment(b); - } - @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { @@ -863,11 +832,6 @@ public class LoginActivity extends BaseActivity Timber.d("NodeFragment placed"); } - void startReceiveFragment(Bundle extras) { - replaceFragment(new ReceiveFragment(), null, extras); - Timber.d("ReceiveFragment placed"); - } - void replaceFragment(Fragment newFragment, String stackName, Bundle extras) { if (extras != null) { newFragment.setArguments(extras); diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index c286d6e2..e6caa3fa 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -82,8 +82,6 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter void onWalletDetails(String wallet); - void onWalletReceive(String wallet); - void onWalletRename(String name); void onWalletBackup(String name); @@ -223,9 +221,6 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter case R.id.action_info: showInfo(listItem.name); break; - case R.id.action_receive: - showReceive(listItem.name); - break; case R.id.action_rename: activityCallback.onWalletRename(listItem.name); break; @@ -290,10 +285,6 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter activityCallback.onWalletDetails(name); } - private void showReceive(@NonNull String name) { - activityCallback.onWalletReceive(name); - } - @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java b/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java index 6e652bfe..2f52f21c 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/NodeFragment.java @@ -228,24 +228,23 @@ public class NodeFragment extends Fragment // open up edit dialog @Override - public void onLongInteraction(final View view, final NodeInfo nodeItem) { + public boolean onLongInteraction(final View view, final NodeInfo nodeItem) { Timber.d("onLongInteraction"); EditDialog diag = createEditDialog(nodeItem); if (diag != null) { diag.show(); } + return true; } @Override public void onClick(View v) { int id = v.getId(); - switch (id) { - case R.id.fab: - EditDialog diag = createEditDialog(null); - if (diag != null) { - diag.show(); - } - break; + if (id == R.id.fab) { + EditDialog diag = createEditDialog(null); + if (diag != null) { + diag.show(); + } } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java index f533a0c2..df3788b4 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -16,7 +16,6 @@ package com.m2049r.xmrwallet; -import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -24,10 +23,11 @@ import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.nfc.NfcManager; -import android.os.AsyncTask; import android.os.Bundle; import android.text.Editable; +import android.text.Html; import android.text.InputType; +import android.text.Spanned; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -47,6 +47,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.ShareActionProvider; +import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.core.view.MenuItemCompat; import androidx.fragment.app.Fragment; @@ -61,11 +62,9 @@ import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.m2049r.xmrwallet.data.BarcodeData; import com.m2049r.xmrwallet.data.Crypto; -import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; +import com.m2049r.xmrwallet.data.Subaddress; import com.m2049r.xmrwallet.model.Wallet; -import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; -import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; import com.m2049r.xmrwallet.util.ThemeHelper; import com.m2049r.xmrwallet.widget.ExchangeView; import com.m2049r.xmrwallet.widget.Toolbar; @@ -82,7 +81,6 @@ import timber.log.Timber; public class ReceiveFragment extends Fragment { private ProgressBar pbProgress; - private TextView tvAddressLabel; private TextView tvAddress; private TextInputLayout etNotes; private ExchangeView evAmount; @@ -91,7 +89,6 @@ public class ReceiveFragment extends Fragment { private ImageView ivQrCodeFull; private EditText etDummy; private ImageButton bCopyAddress; - private ImageButton bSubaddress; private Wallet wallet = null; private boolean isMyWallet = false; @@ -102,6 +99,10 @@ public class ReceiveFragment extends Fragment { void setTitle(String title); void setSubtitle(String subtitle); + + void showSubaddresses(boolean managerMode); + + Subaddress getSelectedSubaddress(); } @Override @@ -111,7 +112,6 @@ public class ReceiveFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_receive, container, false); pbProgress = view.findViewById(R.id.pbProgress); - tvAddressLabel = view.findViewById(R.id.tvAddressLabel); tvAddress = view.findViewById(R.id.tvAddress); etNotes = view.findViewById(R.id.etNotes); evAmount = view.findViewById(R.id.evAmount); @@ -120,13 +120,10 @@ public class ReceiveFragment extends Fragment { ivQrCodeFull = view.findViewById(R.id.qrCodeFull); etDummy = view.findViewById(R.id.etDummy); bCopyAddress = view.findViewById(R.id.bCopyAddress); - bSubaddress = view.findViewById(R.id.bSubaddress); etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); bCopyAddress.setOnClickListener(v -> copyAddress()); - enableCopyAddress(false); - enableSubaddressButton(false); evAmount.setOnNewAmountListener(xmr -> { Timber.d("new amount = %s", xmr); @@ -165,14 +162,8 @@ public class ReceiveFragment extends Fragment { } }); - bSubaddress.setOnClickListener(v -> { - enableSubaddressButton(false); - enableCopyAddress(false); - - final Runnable newAddress = this::getNewSubaddress; - - tvAddress.animate().alpha(0).setDuration(250) - .withEndAction(newAddress).start(); + tvAddress.setOnClickListener(v -> { + listenerCallback.showSubaddresses(false); }); view.findViewById(R.id.cvQrCode).setOnClickListener(v -> { @@ -194,21 +185,11 @@ public class ReceiveFragment extends Fragment { showProgress(); clearQR(); - Bundle b = getArguments(); - String address = b.getString("address"); - String walletName = b.getString("name"); - Timber.d("%s/%s", address, walletName); - if (address == null) { - String path = b.getString("path"); - String password = b.getString("password"); - loadAndShow(path, password); + if (getActivity() instanceof GenerateReviewFragment.ListenerWithWallet) { + wallet = ((GenerateReviewFragment.ListenerWithWallet) getActivity()).getWallet(); + show(); } else { - if (getActivity() instanceof GenerateReviewFragment.ListenerWithWallet) { - wallet = ((GenerateReviewFragment.ListenerWithWallet) getActivity()).getWallet(); - show(); - } else { - throw new IllegalStateException("no wallet info"); - } + throw new IllegalStateException("no wallet info"); } View tvNfc = view.findViewById(R.id.tvNfc); @@ -226,7 +207,7 @@ public class ReceiveFragment extends Fragment { final MaterialContainerTransform transform = new MaterialContainerTransform(); transform.setDrawingViewId(R.id.fragment_container); transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); - transform.setAllContainerColors(ThemeHelper.getThemedColor(Objects.requireNonNull(getContext()), R.attr.colorSurface)); + transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground)); setSharedElementEnterTransition(transform); } @@ -299,12 +280,8 @@ public class ReceiveFragment extends Fragment { return null; } - private void enableSubaddressButton(boolean enable) { - bSubaddress.setEnabled(enable); - } - void copyAddress() { - Helper.clipBoardCopy(Objects.requireNonNull(getActivity()), getString(R.string.label_copy_address), tvAddress.getText().toString()); + Helper.clipBoardCopy(Objects.requireNonNull(getActivity()), getString(R.string.label_copy_address), subaddress.getAddress()); Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show(); } @@ -333,8 +310,9 @@ public class ReceiveFragment extends Fragment { Timber.d("onResume()"); listenerCallback.setToolbarButton(Toolbar.BUTTON_BACK); if (wallet != null) { + listenerCallback.setTitle(wallet.getName()); listenerCallback.setSubtitle(wallet.getAccountLabel()); - generateQr(); + setNewSubaddress(); } else { listenerCallback.setSubtitle(getString(R.string.status_wallet_loading)); clearQR(); @@ -346,93 +324,7 @@ public class ReceiveFragment extends Fragment { private void show() { Timber.d("name=%s", wallet.getName()); isLoaded = true; - listenerCallback.setTitle(wallet.getName()); - listenerCallback.setSubtitle(wallet.getAccountLabel()); - tvAddress.setText(wallet.getAddress()); - enableCopyAddress(true); - enableSubaddressButton(true); hideProgress(); - generateQr(); - } - - private void enableCopyAddress(boolean enable) { - bCopyAddress.setEnabled(enable); - } - - private void loadAndShow(String walletPath, String password) { - new AsyncShow(walletPath, password).executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); - } - - GenerateReviewFragment.ProgressListener progressCallback = null; - - @SuppressLint("StaticFieldLeak") - private class AsyncShow extends AsyncTask { - final private String walletPath; - final private String password; - - - AsyncShow(String walletPath, String passsword) { - super(); - this.walletPath = walletPath; - this.password = passsword; - } - - boolean dialogOpened = false; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgress(); - if ((walletPath != null) - && (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", password) - == Wallet.Device.Device_Ledger) - && (progressCallback != null)) { - progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE); - dialogOpened = true; - } - } - - @Override - protected Boolean doInBackground(Void... params) { - if (params.length != 0) return false; - wallet = WalletManager.getInstance().openWallet(walletPath, password); - isMyWallet = true; - return true; - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (dialogOpened) - progressCallback.dismissProgressDialog(); - if (!isAdded()) return; // never mind - if (result) { - show(); - } else { - Toast.makeText(getActivity(), getString(R.string.receive_cannot_open), Toast.LENGTH_LONG).show(); - hideProgress(); - } - } - } - - private void storeWallet() { - new AsyncStore().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); - } - - @SuppressLint("StaticFieldLeak") - private class AsyncStore extends AsyncTask { - @Override - protected Boolean doInBackground(String... params) { - if (params.length != 0) return false; - if (wallet != null) wallet.store(); - return true; - } - - @Override - protected void onPostExecute(Boolean result) { - enableSubaddressButton(true); - super.onPostExecute(result); - } } public BarcodeData getBarcodeData() { @@ -446,7 +338,7 @@ public class ReceiveFragment extends Fragment { private void generateQr() { Timber.d("GENQR"); - String address = tvAddress.getText().toString(); + String address = subaddress.getAddress(); String notes = etNotes.getEditText().getText().toString(); String xmrAmount = evAmount.getAmount(); Timber.d("%s/%s/%s", xmrAmount, notes, address); @@ -543,9 +435,6 @@ public class ReceiveFragment extends Fragment { throw new ClassCastException(context.toString() + " must implement Listener"); } - if (context instanceof GenerateReviewFragment.ProgressListener) { - this.progressCallback = (GenerateReviewFragment.ProgressListener) context; - } } @Override @@ -566,49 +455,23 @@ public class ReceiveFragment extends Fragment { super.onDetach(); } - private void getNewSubaddress() { - new AsyncSubaddress().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); - } + private Subaddress subaddress = null; - @SuppressLint("StaticFieldLeak") - private class AsyncSubaddress extends AsyncTask { - private String newSubaddress; - - boolean dialogOpened = false; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if ((wallet.getDeviceType() == Wallet.Device.Device_Ledger) && (progressCallback != null)) { - progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS); - dialogOpened = true; - } - } - - @Override - protected Boolean doInBackground(Void... params) { - if (params.length != 0) return false; - newSubaddress = wallet.getNewSubaddress(); - storeWallet(); - return true; - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (dialogOpened) - progressCallback.dismissProgressDialog(); - if (!isAdded()) // never mind then - return; - tvAddress.setText(newSubaddress); - tvAddressLabel.setText(getString(R.string.generate_address_label_sub, - wallet.getNumSubaddresses() - 1)); - generateQr(); - enableCopyAddress(true); + void setNewSubaddress() { + final Subaddress newSubaddress = listenerCallback.getSelectedSubaddress(); + if (!Objects.equals(subaddress, newSubaddress)) { final Runnable resetSize = () -> tvAddress.animate().setDuration(125).scaleX(1).scaleY(1).start(); tvAddress.animate().alpha(1).setDuration(125) .scaleX(1.2f).scaleY(1.2f) .withEndAction(resetSize).start(); } + subaddress = newSubaddress; + final Context context = getContext(); + Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress, + Integer.toHexString(ContextCompat.getColor(context, R.color.monerujoGreen) & 0xFFFFFF), + Integer.toHexString(ContextCompat.getColor(context, R.color.monerujoBackground) & 0xFFFFFF), + subaddress.getDisplayLabel(), subaddress.getAddress())); + tvAddress.setText(label); + generateQr(); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java new file mode 100644 index 00000000..45c0678a --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/SubaddressFragment.java @@ -0,0 +1,246 @@ +/* + * 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.annotation.SuppressLint; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.transition.MaterialElevationScale; +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.layout.SubaddressInfoAdapter; +import com.m2049r.xmrwallet.ledger.LedgerProgressDialog; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.util.ArrayList; +import java.util.List; + +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +public class SubaddressFragment extends Fragment implements SubaddressInfoAdapter.OnInteractionListener, + View.OnClickListener { + static public final String KEY_MODE = "mode"; + static public final String MODE_MANAGER = "manager"; + + private SubaddressInfoAdapter adapter; + + private Listener activityCallback; + + private Wallet wallet; + + // Container Activity must implement this interface + public interface Listener { + void onSubaddressSelected(Subaddress subaddress); + + void setSubtitle(String title); + + void setToolbarButton(int type); + + void showSubaddress(View view, final int subaddressIndex); + } + + public interface ProgressListener { + void showProgressDialog(int msgId); + + void showLedgerProgressDialog(int mode); + + void dismissProgressDialog(); + } + + private ProgressListener progressCallback = null; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof ProgressListener) { + progressCallback = (ProgressListener) context; + } + if (context instanceof Listener) { + activityCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onPause() { + Timber.d("onPause()"); + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + activityCallback.setSubtitle(getString(R.string.subbaddress_title)); + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Timber.d("onCreateView"); + + final Bundle b = getArguments(); + managerMode = ((b != null) && (MODE_MANAGER.equals(b.getString(KEY_MODE)))); + + View view = inflater.inflate(R.layout.fragment_subaddress, container, false); + + final MaterialElevationScale exitTransition = new MaterialElevationScale(false); + exitTransition.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); + setExitTransition(exitTransition); + final MaterialElevationScale reenterTransition = new MaterialElevationScale(true); + reenterTransition.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); + setReenterTransition(reenterTransition); + + view.findViewById(R.id.fab).setOnClickListener(this); + + if (managerMode) { + view.findViewById(R.id.tvInstruction).setVisibility(View.GONE); + view.findViewById(R.id.tvHint).setVisibility(View.GONE); + } + + final RecyclerView list = view.findViewById(R.id.list); + adapter = new SubaddressInfoAdapter(getActivity(), this); + list.setAdapter(adapter); + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + list.scrollToPosition(positionStart); + } + }); + + Helper.hideKeyboard(getActivity()); + + wallet = WalletManager.getInstance().getWallet(); + + loadList(); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + postponeEnterTransition(); + view.getViewTreeObserver().addOnPreDrawListener(() -> { + startPostponedEnterTransition(); + return true; + }); + } + + public void loadList() { + Timber.d("loadList()"); + final int numSubaddresses = wallet.getNumSubaddresses(); + final List list = new ArrayList<>(); + for (int i = 0; i < numSubaddresses; i++) { + list.add(wallet.getSubaddressObject(i)); + } + adapter.setInfos(list); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.fab) { + getNewSubaddress(); + } + } + + private int lastUsedSubaddress() { + int lastUsedSubaddress = 0; + for (TransactionInfo info : wallet.getHistory().getAll()) { + if (info.addressIndex > lastUsedSubaddress) + lastUsedSubaddress = info.addressIndex; + } + return lastUsedSubaddress; + } + + private void getNewSubaddress() { + final int maxSubaddresses = lastUsedSubaddress() + wallet.getDeviceType().getSubaddressLookahead(); + if (wallet.getNumSubaddresses() < maxSubaddresses) + new AsyncSubaddress().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + else + Toast.makeText(getActivity(), getString(R.string.max_subaddress_warning), Toast.LENGTH_LONG).show(); + } + + @SuppressLint("StaticFieldLeak") + @RequiredArgsConstructor + private class AsyncSubaddress extends AsyncTask { + boolean dialogOpened = false; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if ((wallet.getDeviceType() == Wallet.Device.Device_Ledger) && (progressCallback != null)) { + progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS); + dialogOpened = true; + } + } + + @Override + protected Boolean doInBackground(Void... params) { + if (params.length != 0) return false; + wallet.getNewSubaddress(); + wallet.store(); + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (dialogOpened) + progressCallback.dismissProgressDialog(); + if (!isAdded()) // never mind then + return; + loadList(); + } + } + + boolean managerMode = false; + + // Callbacks from SubaddressInfoAdapter + @Override + public void onInteraction(final View view, final Subaddress subaddress) { + if (managerMode) + activityCallback.showSubaddress(view, subaddress.getAddressIndex()); + else + activityCallback.onSubaddressSelected(subaddress); // also closes the fragment with onBackpressed() + } + + @Override + public boolean onLongInteraction(View view, Subaddress subaddress) { + activityCallback.showSubaddress(view, subaddress.getAddressIndex()); + return false; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java new file mode 100644 index 00000000..897dcd0c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/SubaddressInfoFragment.java @@ -0,0 +1,170 @@ +/* + * 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.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.textfield.TextInputLayout; +import com.google.android.material.transition.MaterialContainerTransform; +import com.m2049r.xmrwallet.data.Subaddress; +import com.m2049r.xmrwallet.layout.TransactionInfoAdapter; +import com.m2049r.xmrwallet.model.TransactionInfo; +import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.widget.Toolbar; + +import java.util.ArrayList; +import java.util.List; + +import timber.log.Timber; + +// TODO: live update - i.e. use onRefreshed() somehow +public class SubaddressInfoFragment extends Fragment implements TransactionInfoAdapter.OnInteractionListener { + private TransactionInfoAdapter adapter; + + private Subaddress subaddress; + + private TextInputLayout etName; + private TextView tvAddress; + private TextView tvTxLabel; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false); + + etName = view.findViewById(R.id.etName); + tvAddress = view.findViewById(R.id.tvAddress); + tvTxLabel = view.findViewById(R.id.tvTxLabel); + + final RecyclerView list = view.findViewById(R.id.list); + adapter = new TransactionInfoAdapter(getActivity(), this); + list.setAdapter(adapter); + + final Wallet wallet = activityCallback.getWallet(); + + Bundle b = getArguments(); + final int subaddressIndex = b.getInt("subaddressIndex"); + subaddress = wallet.getSubaddressObject(subaddressIndex); + + etName.getEditText().setText(subaddress.getDisplayLabel()); + tvAddress.setText(getContext().getString(R.string.subbaddress_info_subtitle, + subaddress.getAddressIndex(), subaddress.getSquashedAddress())); + + etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString()); + } + }); + etName.getEditText().setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + Helper.hideKeyboard(getActivity()); + wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString()); + onRefreshed(wallet); + return true; + } + return false; + }); + + onRefreshed(wallet); + + return view; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final MaterialContainerTransform transform = new MaterialContainerTransform(); + transform.setDrawingViewId(R.id.fragment_container); + transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); + transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground)); + setSharedElementEnterTransition(transform); + } + + public void onRefreshed(final Wallet wallet) { + Timber.d("onRefreshed"); + List list = new ArrayList<>(); + for (TransactionInfo info : wallet.getHistory().getAll()) { + if (info.addressIndex == subaddress.getAddressIndex()) + list.add(info); + } + adapter.setInfos(list); + if (list.isEmpty()) + tvTxLabel.setText(R.string.subaddress_notx_label); + else + tvTxLabel.setText(R.string.subaddress_tx_label); + } + + // Callbacks from TransactionInfoAdapter + @Override + public void onInteraction(final View view, final TransactionInfo infoItem) { + activityCallback.onTxDetailsRequest(view, infoItem); + } + + Listener activityCallback; + + // Container Activity must implement this interface + public interface Listener { + void onTxDetailsRequest(View view, TransactionInfo info); + + Wallet getWallet(); + + void setToolbarButton(int type); + + void setTitle(String title, String subtitle); + + void setSubtitle(String subtitle); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof Listener) { + this.activityCallback = (Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setSubtitle(getString(R.string.subbaddress_title)); + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + } + + @Override + public void onPause() { + super.onPause(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java index 9d1a69d2..cca468a6 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/TxFragment.java @@ -22,7 +22,9 @@ import android.content.Intent; import android.graphics.Paint; import android.net.Uri; import android.os.Bundle; +import android.text.Html; import android.text.InputType; +import android.text.Spanned; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -37,12 +39,14 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import com.google.android.material.transition.MaterialContainerTransform; +import com.google.android.material.transition.MaterialElevationScale; +import com.m2049r.xmrwallet.data.Subaddress; import com.m2049r.xmrwallet.data.UserNotes; import com.m2049r.xmrwallet.model.TransactionInfo; import com.m2049r.xmrwallet.model.Transfer; import com.m2049r.xmrwallet.model.Wallet; -import com.m2049r.xmrwallet.util.ThemeHelper; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.ThemeHelper; import com.m2049r.xmrwallet.widget.Toolbar; import java.text.SimpleDateFormat; @@ -52,6 +56,8 @@ import java.util.HashSet; import java.util.Set; import java.util.TimeZone; +import timber.log.Timber; + public class TxFragment extends Fragment { static public final String ARG_INFO = "info"; @@ -88,11 +94,16 @@ public class TxFragment extends Fragment { private ImageView tvXmrToLogo; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_tx_info, container, false); + final MaterialElevationScale exitTransition = new MaterialElevationScale(false); + exitTransition.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); + setExitTransition(exitTransition); + final MaterialElevationScale reenterTransition = new MaterialElevationScale(true); + reenterTransition.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); + setReenterTransition(reenterTransition); + cvXmrTo = view.findViewById(R.id.cvXmrTo); tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey); tvDestinationBtc = view.findViewById(R.id.tvDestinationBtc); @@ -121,9 +132,8 @@ public class TxFragment extends Fragment { Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show(); }); - Bundle args = getArguments(); - TransactionInfo info = args.getParcelable(ARG_INFO); - show(info); + info = getArguments().getParcelable(ARG_INFO); + show(); return view; } @@ -192,7 +202,7 @@ public class TxFragment extends Fragment { TransactionInfo info = null; UserNotes userNotes = null; - void loadNotes(TransactionInfo info) { + void loadNotes() { if ((userNotes == null) || (info.notes == null)) { info.notes = activityCallback.getTxNotes(info.hash); } @@ -205,19 +215,28 @@ public class TxFragment extends Fragment { tvTxFee.setTextColor(clr); } - private void show(TransactionInfo info) { + private void showSubaddressLabel() { + final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex); + final Context ctx = getContext(); + Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted, + info.accountIndex, info.addressIndex, + Integer.toHexString(ContextCompat.getColor(ctx, R.color.monerujoGreen) & 0xFFFFFF), + Integer.toHexString(ContextCompat.getColor(ctx, R.color.monerujoBackground) & 0xFFFFFF), + subaddress.getDisplayLabel())); + tvAccount.setText(label); + tvAccount.setOnClickListener(v -> activityCallback.showSubaddress(v, info.addressIndex)); + } + + private void show() { if (info.txKey == null) { info.txKey = activityCallback.getTxKey(info.hash); } if (info.address == null) { - info.address = activityCallback.getTxAddress(info.account, info.subaddress); + info.address = activityCallback.getTxAddress(info.accountIndex, info.addressIndex); } - loadNotes(info); + loadNotes(); - activityCallback.setSubtitle(getString(R.string.tx_title)); - activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); - - tvAccount.setText(getString(R.string.tx_account_formatted, info.account, info.subaddress)); + showSubaddressLabel(); tvAddress.setText(info.address); tvTxTimestamp.setText(TS_FORMATTER.format(new Date(info.timestamp * 1000))); @@ -256,8 +275,8 @@ public class TxFragment extends Fragment { setTxColour(ContextCompat.getColor(getContext(), R.color.tx_minus)); } Set destinations = new HashSet<>(); - StringBuffer sb = new StringBuffer(); - StringBuffer dstSb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); + StringBuilder dstSb = new StringBuilder(); if (info.transfers != null) { boolean newline = false; for (Transfer transfer : info.transfers) { @@ -281,14 +300,12 @@ public class TxFragment extends Fragment { } } else { sb.append("-"); - dstSb.append(info.direction == - TransactionInfo.Direction.Direction_In ? - activityCallback.getWalletSubaddress(info.account, info.subaddress) : + dstSb.append(info.direction == TransactionInfo.Direction.Direction_In ? + activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex).getAddress() : "-"); } tvTxTransfers.setText(sb.toString()); tvDestination.setText(dstSb.toString()); - this.info = info; showBtcInfo(); } @@ -334,7 +351,7 @@ public class TxFragment extends Fragment { final MaterialContainerTransform transform = new MaterialContainerTransform(); transform.setDrawingViewId(R.id.fragment_container); transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); - transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), R.attr.colorSurface)); + transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground)); setSharedElementEnterTransition(transform); } @@ -347,7 +364,7 @@ public class TxFragment extends Fragment { Listener activityCallback; public interface Listener { - String getWalletSubaddress(int accountIndex, int subaddressIndex); + Subaddress getWalletSubaddress(int accountIndex, int subaddressIndex); String getTxKey(String hash); @@ -361,6 +378,8 @@ public class TxFragment extends Fragment { void setSubtitle(String subtitle); + void showSubaddress(View view, final int subaddressIndex); + } @Override @@ -385,4 +404,13 @@ public class TxFragment extends Fragment { Helper.hideKeyboard(getActivity()); super.onPause(); } + + @Override + public void onResume() { + super.onResume(); + Timber.d("onResume()"); + activityCallback.setSubtitle(getString(R.string.tx_title)); + activityCallback.setToolbarButton(Toolbar.BUTTON_BACK); + showSubaddressLabel(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 2e862e1f..93552e75 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -38,6 +38,7 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AlertDialog; import androidx.core.view.GravityCompat; @@ -48,6 +49,7 @@ import androidx.fragment.app.FragmentManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.navigation.NavigationView; import com.m2049r.xmrwallet.data.BarcodeData; +import com.m2049r.xmrwallet.data.Subaddress; import com.m2049r.xmrwallet.data.TxData; import com.m2049r.xmrwallet.data.UserNotes; import com.m2049r.xmrwallet.dialog.CreditsFragment; @@ -78,7 +80,9 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste ScannerFragment.OnScannedListener, ReceiveFragment.Listener, SendAddressWizardFragment.OnScanListener, WalletFragment.DrawerLocker, - NavigationView.OnNavigationItemSelectedListener { + NavigationView.OnNavigationItemSelectedListener, + SubaddressFragment.Listener, + SubaddressInfoFragment.Listener { public static final String REQUEST_ID = "id"; public static final String REQUEST_PW = "pw"; @@ -291,6 +295,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste HelpFragment.display(getSupportFragmentManager(), R.string.help_send); } else if (itemId == R.id.action_rename) { onAccountRename(); + } else if (itemId == R.id.action_subaddresses) { + showSubaddresses(true); } else if (itemId == R.id.action_streetmode) { if (isStreetMode()) { // disable streetmode onDisableStreetMode(); @@ -764,8 +770,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste } @Override - public String getWalletSubaddress(int accountIndex, int subaddressIndex) { - return getWallet().getSubaddress(accountIndex, subaddressIndex); + public Subaddress getWalletSubaddress(int accountIndex, int subaddressIndex) { + return getWallet().getSubaddressObject(accountIndex, subaddressIndex); } public String getWalletName() { @@ -791,6 +797,8 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste transition = R.string.receive_transition_name; else if (newFragment instanceof SendFragment) transition = R.string.send_transition_name; + else if (newFragment instanceof SubaddressInfoFragment) + transition = R.string.subaddress_info_transition_name; else throw new IllegalStateException("expecting known transition"); @@ -889,7 +897,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste } else { Timber.i("Waiting for permissions"); } - } @Override @@ -934,7 +941,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste startScanFragment = true; } else { String msg = getString(R.string.message_camera_not_permitted); - Timber.e(msg); Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); } } @@ -970,6 +976,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste } else { super.onBackPressed(); } + Helper.hideKeyboard(this); } @Override @@ -1095,6 +1102,11 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste dialog.show(); } + public void setAccountIndex(int accountIndex) { + getWallet().setAccountIndex(accountIndex); + selectedSubaddressIndex = 0; + } + @Override public boolean onNavigationItemSelected(MenuItem item) { final int id = item.getItemId(); @@ -1105,7 +1117,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste int accountIdx = accountIds.indexOf(id); if (accountIdx >= 0) { Timber.d("found @%d", accountIdx); - getWallet().setAccountIndex(accountIdx); + setAccountIndex(accountIdx); } forceUpdate(); drawer.closeDrawer(GravityCompat.START); @@ -1113,8 +1125,22 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste return true; } + private int lastUsedAccount() { + int lastUsedAccount = 0; + for (TransactionInfo info : getWallet().getHistory().getAll()) { + if (info.accountIndex > lastUsedAccount) + lastUsedAccount = info.accountIndex; + } + return lastUsedAccount; + } + private void addAccount() { - new AsyncAddAccount().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + final Wallet wallet = getWallet(); + final int maxAccounts = lastUsedAccount() + wallet.getDeviceType().getAccountLookahead(); + if (wallet.getNumAccounts() < maxAccounts) + new AsyncAddAccount().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR); + else + Toast.makeText(this, getString(R.string.max_account_warning), Toast.LENGTH_LONG).show(); } @SuppressLint("StaticFieldLeak") @@ -1142,7 +1168,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste protected Boolean doInBackground(Void... params) { if (params.length != 0) return false; getWallet().addAccount(); - getWallet().setAccountIndex(getWallet().getNumAccounts() - 1); + setAccountIndex(getWallet().getNumAccounts() - 1); return true; } @@ -1158,4 +1184,34 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste Toast.LENGTH_SHORT).show(); } } + + // we store the index only and always retrieve a new Subaddress object + // to ensure we get the current label + private int selectedSubaddressIndex = 0; + + @Override + public Subaddress getSelectedSubaddress() { + return getWallet().getSubaddressObject(selectedSubaddressIndex); + } + + @Override + public void onSubaddressSelected(@Nullable final Subaddress subaddress) { + selectedSubaddressIndex = subaddress.getAddressIndex(); + onBackPressed(); + } + + @Override + public void showSubaddresses(boolean managerMode) { + final Bundle b = new Bundle(); + if (managerMode) + b.putString(SubaddressFragment.KEY_MODE, SubaddressFragment.MODE_MANAGER); + replaceFragment(new SubaddressFragment(), null, b); + } + + @Override + public void showSubaddress(View view, final int subaddressIndex) { + final Bundle b = new Bundle(); + b.putInt("subaddressIndex", subaddressIndex); + replaceFragmentWithTransition(view, new SubaddressInfoFragment(), null, b); + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java index 60e0f46d..d365cf8b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -79,6 +79,7 @@ public class WalletFragment extends Fragment private Button bSend; private ImageView ivStreetGunther; private Drawable streetGunther = null; + RecyclerView txlist; private Spinner sCurrency; @@ -106,13 +107,6 @@ public class WalletFragment extends Fragment Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_wallet, container, false); - final MaterialElevationScale exitTransition = new MaterialElevationScale(false); - exitTransition.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); - setExitTransition(exitTransition); - final MaterialElevationScale reenterTransition = new MaterialElevationScale(true); - reenterTransition.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); - setReenterTransition(reenterTransition); - ivStreetGunther = view.findViewById(R.id.ivStreetGunther); tvStreetView = view.findViewById(R.id.tvStreetView); llBalance = view.findViewById(R.id.llBalance); @@ -140,13 +134,19 @@ public class WalletFragment extends Fragment bSend = view.findViewById(R.id.bSend); bReceive = view.findViewById(R.id.bReceive); - RecyclerView recyclerView = view.findViewById(R.id.list); - + txlist = view.findViewById(R.id.list); adapter = new TransactionInfoAdapter(getActivity(), this); - recyclerView.setAdapter(adapter); + txlist.setAdapter(adapter); + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + if ((positionStart == 0) && (txlist.computeVerticalScrollOffset() == 0)) + txlist.scrollToPosition(positionStart); + } + }); - SwipeableRecyclerViewTouchListener swipeTouchListener = - new SwipeableRecyclerViewTouchListener(recyclerView, + txlist.addOnItemTouchListener( + new SwipeableRecyclerViewTouchListener(txlist, new SwipeableRecyclerViewTouchListener.SwipeListener() { @Override public boolean canSwipeLeft(int position) { @@ -173,10 +173,7 @@ public class WalletFragment extends Fragment adapter.removeItem(position); } } - }); - - recyclerView.addOnItemTouchListener(swipeTouchListener); - + })); bSend.setOnClickListener(v -> activityCallback.onSendRequest(v)); bReceive.setOnClickListener(v -> activityCallback.onWalletReceive(v)); @@ -338,24 +335,35 @@ public class WalletFragment extends Fragment final MaterialElevationScale reenterTransition = new MaterialElevationScale(true); reenterTransition.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); setReenterTransition(reenterTransition); + activityCallback.onTxDetailsRequest(view, infoItem); } // called from activity + + // if account index has changed scroll to top? + private int accountIndex = 0; + public void onRefreshed(final Wallet wallet, final boolean full) { Timber.d("onRefreshed(%b)", full); + if (full) { List list = new ArrayList<>(); final long streetHeight = activityCallback.getStreetModeHeight(); Timber.d("StreetHeight=%d", streetHeight); for (TransactionInfo info : wallet.getHistory().getAll()) { - Timber.d("TxHeight=%d", info.blockheight); + Timber.d("TxHeight=%d, Label=%s", info.blockheight, info.subaddressLabel); if ((info.isPending || (info.blockheight >= streetHeight)) && !dismissedTransactions.contains(info.hash)) list.add(info); } adapter.setInfos(list); + if (accountIndex != wallet.getAccountIndex()) { + accountIndex = wallet.getAccountIndex(); + txlist.scrollToPosition(0); + } + } updateStatus(wallet); } @@ -522,9 +530,10 @@ public class WalletFragment extends Fragment @Override public void onResume() { super.onResume(); + setExitTransition(null); + setReenterTransition(null); Timber.d("onResume()"); activityCallback.setTitle(walletTitle, walletSubtitle); - //activityCallback.setToolbarButton(Toolbar.BUTTON_CLOSE); // TODO: Close button somewhere else activityCallback.setToolbarButton(Toolbar.BUTTON_NONE); setProgress(syncProgress); setProgress(syncText); diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java b/app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java new file mode 100644 index 00000000..6a79dfdb --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/data/Subaddress.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018 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.data; + +import java.util.regex.Pattern; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@ToString +@EqualsAndHashCode +public class Subaddress implements Comparable { + @Getter + final private int accountIndex; + @Getter + final private int addressIndex; + @Getter + final private String address; + @Getter + private final String label; + + @Override + public int compareTo(Subaddress another) { // newer is < + final int compareAccountIndex = another.accountIndex - accountIndex; + if (compareAccountIndex == 0) + return another.addressIndex - addressIndex; + return compareAccountIndex; + } + + public String getSquashedAddress() { + return address.substring(0, 8) + "…" + address.substring(address.length() - 8); + } + + public static final Pattern DEFAULT_LABEL_FORMATTER = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}:[0-9]{2}:[0-9]{2}$"); + + public String getDisplayLabel() { + if (label.isEmpty() || (DEFAULT_LABEL_FORMATTER.matcher(label).matches())) + return ("#" + addressIndex); + else + return label; + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java index d480cc74..d77cc21a 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendFragment.java @@ -547,7 +547,7 @@ public class SendFragment extends Fragment final MaterialContainerTransform transform = new MaterialContainerTransform(); transform.setDrawingViewId(R.id.fragment_container); transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration)); - transform.setAllContainerColors(ThemeHelper.getThemedColor(Objects.requireNonNull(getContext()), R.attr.colorSurface)); + transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground)); setSharedElementEnterTransition(transform); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java index 5d8c52d6..c93911ee 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/NodeInfoAdapter.java @@ -48,7 +48,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter nodeItems = new ArrayList<>(); @@ -213,10 +213,10 @@ public class NodeInfoAdapter extends RecyclerView.Adapter { + public interface OnInteractionListener { + void onInteraction(View view, Subaddress item); + + boolean onLongInteraction(View view, Subaddress item); + } + + private final List items; + private final OnInteractionListener listener; + + Context context; + + public SubaddressInfoAdapter(Context context, OnInteractionListener listener) { + this.context = context; + this.items = new ArrayList<>(); + this.listener = listener; + } + + private static class SubaddressInfoDiff extends DiffCallback { + + public SubaddressInfoDiff(List oldList, List newList) { + super(oldList, newList); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).getAddress().equals(mNewList.get(newItemPosition).getAddress()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition)); + } + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_subaddress, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + holder.bind(position); + } + + @Override + public int getItemCount() { + return items.size(); + } + + public Subaddress getItem(int position) { + return items.get(position); + } + + public void setInfos(List newItems) { + if (newItems == null) { + newItems = new ArrayList<>(); + Timber.d("setInfos null"); + } else { + Timber.d("setInfos %s", newItems.size()); + } + Collections.sort(newItems); + final DiffCallback diffCallback = new SubaddressInfoDiff(items, newItems); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + items.clear(); + items.addAll(newItems); + diffResult.dispatchUpdatesTo(this); + } + + class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + final TextView tvName; + final TextView tvAddress; + Subaddress item; + + ViewHolder(View itemView) { + super(itemView); + tvName = itemView.findViewById(R.id.tvName); + tvAddress = itemView.findViewById(R.id.tvAddress); + itemView.setOnClickListener(this); + itemView.setOnLongClickListener(this); + } + + void bind(int position) { + item = getItem(position); + itemView.setTransitionName(context.getString(R.string.subaddress_item_transition_name, item.getAddressIndex())); + + final String label = item.getDisplayLabel(); + final String address = context.getString(R.string.subbaddress_info_subtitle, + item.getAddressIndex(), item.getSquashedAddress()); + tvName.setText(label.isEmpty() ? address : label); + tvAddress.setText(address); + } + + @Override + public void onClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + listener.onInteraction(view, getItem(position)); + } + } + } + + @Override + public boolean onLongClick(View view) { + if (listener != null) { + int position = getAdapterPosition(); // gets item position + if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it + return listener.onLongInteraction(view, getItem(position)); + } + } + return true; + } + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java index 4f5a7952..cb21a929 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java @@ -17,6 +17,8 @@ package com.m2049r.xmrwallet.layout; import android.content.Context; +import android.text.Html; +import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -46,7 +48,6 @@ import java.util.TimeZone; import timber.log.Timber; public class TransactionInfoAdapter extends RecyclerView.Adapter { - private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm"); private final int outboundColour; @@ -94,6 +95,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter t = refreshJ(); - Timber.d("refreshed %d", t.size()); - for (Iterator iterator = t.iterator(); iterator.hasNext(); ) { + List transactionInfos = refreshJ(); + Timber.d("refreshed %d", transactionInfos.size()); + for (Iterator iterator = transactionInfos.iterator(); iterator.hasNext(); ) { TransactionInfo info = iterator.next(); - if (info.account != accountIndex) { + if (info.accountIndex != accountIndex) { iterator.remove(); Timber.d("removed %s", info.hash); } else { Timber.d("kept %s", info.hash); } } - transactions = t; + transactions = transactionInfos; } private native List refreshJ(); diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java index ba123cf6..17af6803 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/TransactionInfo.java @@ -19,13 +19,17 @@ package com.m2049r.xmrwallet.model; import android.os.Parcel; import android.os.Parcelable; +import com.m2049r.xmrwallet.data.Subaddress; + import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + // 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 implements Parcelable, Comparable { - static final String TAG = "TransactionInfo"; - + @RequiredArgsConstructor public enum Direction { Direction_In(0), Direction_Out(1); @@ -40,15 +44,8 @@ public class TransactionInfo implements Parcelable, Comparable return null; } - public int getValue() { - return value; - } - - private int value; - - Direction(int value) { - this.value = value; - } + @Getter + private final int value; } public Direction direction; @@ -60,8 +57,8 @@ public class TransactionInfo implements Parcelable, Comparable public String hash; public long timestamp; public String paymentId; - public int account; - public int subaddress; + public int accountIndex; + public int addressIndex; public long confirmations; public String subaddressLabel; public List transfers; @@ -80,8 +77,8 @@ public class TransactionInfo implements Parcelable, Comparable String hash, long timestamp, String paymentId, - int account, - int subaddress, + int accountIndex, + int addressIndex, long confirmations, String subaddressLabel, List transfers) { @@ -94,13 +91,20 @@ public class TransactionInfo implements Parcelable, Comparable this.hash = hash; this.timestamp = timestamp; this.paymentId = paymentId; - this.account = account; - this.subaddress = subaddress; + this.accountIndex = accountIndex; + this.addressIndex = addressIndex; this.confirmations = confirmations; this.subaddressLabel = subaddressLabel; this.transfers = transfers; } + public String getDisplayLabel() { + if (subaddressLabel.isEmpty() || (Subaddress.DEFAULT_LABEL_FORMATTER.matcher(subaddressLabel).matches())) + return ("#" + addressIndex); + else + return subaddressLabel; + } + public String toString() { return direction + "@" + blockheight + " " + amount; } @@ -116,8 +120,8 @@ public class TransactionInfo implements Parcelable, Comparable out.writeString(hash); out.writeLong(timestamp); out.writeString(paymentId); - out.writeInt(account); - out.writeInt(subaddress); + out.writeInt(accountIndex); + out.writeInt(addressIndex); out.writeLong(confirmations); out.writeString(subaddressLabel); out.writeList(transfers); @@ -146,8 +150,8 @@ public class TransactionInfo implements Parcelable, Comparable hash = in.readString(); timestamp = in.readLong(); paymentId = in.readString(); - account = in.readInt(); - subaddress = in.readInt(); + accountIndex = in.readInt(); + addressIndex = in.readInt(); confirmations = in.readLong(); subaddressLabel = in.readString(); transfers = in.readArrayList(Transfer.class.getClassLoader()); 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 4928d77a..765ad731 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -19,6 +19,7 @@ package com.m2049r.xmrwallet.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.m2049r.xmrwallet.data.Subaddress; import com.m2049r.xmrwallet.data.TxData; import java.io.File; @@ -26,6 +27,8 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import timber.log.Timber; public class Wallet { @@ -104,10 +107,14 @@ public class Wallet { this.accountIndex = accountIndex; } + @RequiredArgsConstructor + @Getter public enum Device { - Device_Undefined, - Device_Software, - Device_Ledger + Device_Undefined(0, 0), + Device_Software(50, 200), + Device_Ledger(5, 20); + private final int accountLookahead; + private final int subaddressLookahead; } public enum StatusEnum { @@ -160,6 +167,15 @@ public class Wallet { private native String getAddressJ(int accountIndex, int addressIndex); + public Subaddress getSubaddressObject(int accountIndex, int subAddressIndex) { + return new Subaddress(accountIndex, subAddressIndex, + getSubaddress(subAddressIndex), getSubaddressLabel(subAddressIndex)); + } + + public Subaddress getSubaddressObject(int subAddressIndex) { + return getSubaddressObject(accountIndex, subAddressIndex); + } + public native String getPath(); public NetworkType getNetworkType() { @@ -428,6 +444,11 @@ public class Wallet { setSubaddressLabel(accountIndex, 0, label); } + public void setSubaddressLabel(int addressIndex, String label) { + setSubaddressLabel(accountIndex, addressIndex, label); + getHistory().refreshWithNotes(this); + } + public native void setSubaddressLabel(int accountIndex, int addressIndex, String label); public native int getNumAccounts(); diff --git a/app/src/main/res/layout/fragment_receive.xml b/app/src/main/res/layout/fragment_receive.xml index f3cee81a..0290b015 100644 --- a/app/src/main/res/layout/fragment_receive.xml +++ b/app/src/main/res/layout/fragment_receive.xml @@ -26,18 +26,27 @@ android:indeterminate="true" android:visibility="gone" /> + + + android:orientation="horizontal"> + android:id="@+id/tvAddress" + style="@style/MoneroText.Medium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_weight="1" + tools:text="TAG 9wZnnNctRc7RaLya1rxykH21dUwfQpNGmVLjAvkvqe7nKT2Mw848AJNGMunW5xjoSZ5vCCU3uDnUoVqSSHxzRtQBE3f6crx" /> - - - - - - - diff --git a/app/src/main/res/layout/fragment_subaddress.xml b/app/src/main/res/layout/fragment_subaddress.xml new file mode 100644 index 00000000..ef34160e --- /dev/null +++ b/app/src/main/res/layout/fragment_subaddress.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subaddressinfo.xml b/app/src/main/res/layout/fragment_subaddressinfo.xml new file mode 100644 index 00000000..e84c7f0f --- /dev/null +++ b/app/src/main/res/layout/fragment_subaddressinfo.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_tx_info.xml b/app/src/main/res/layout/fragment_tx_info.xml index fef5b62d..95a256fa 100644 --- a/app/src/main/res/layout/fragment_tx_info.xml +++ b/app/src/main/res/layout/fragment_tx_info.xml @@ -206,9 +206,8 @@ android:paddingStart="8dp" android:paddingTop="8dp" android:paddingEnd="8dp" - android:selectAllOnFocus="true" - android:textIsSelectable="true" - tools:text="(0,1)" /> + android:transitionName="@string/subaddress_txinfo_transition_name" + tools:text="(0,1) #17" /> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/list_context_menu.xml b/app/src/main/res/menu/list_context_menu.xml index 2577452d..c43925c7 100644 --- a/app/src/main/res/menu/list_context_menu.xml +++ b/app/src/main/res/menu/list_context_menu.xml @@ -6,11 +6,6 @@ android:orderInCategory="50" android:title="@string/menu_streetmode" /> - - + + Compartir Ajuda Rebre - Canvi de nom … + Canvi de nom Arxivar Còpia de seguretat Canvi de contrasenya @@ -287,8 +287,8 @@ Compte # Envia tots els fons confirmats cap aquest compte! - Subadreces #%1$d - Subadreces Públiques #%1$d + Subadreces + Subadreces Públiques #%1$d: %2$s Idioma Utilitzar Idioma del Sistema @@ -410,4 +410,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 94d5ee5f..3b90f307 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -8,7 +8,7 @@ Teilen Hilfe Empfangen - Umbenennen … + Umbenennen Archivieren Backup Passwort ändern @@ -280,8 +280,8 @@ Konto # Versende ALLE(!) verfügbaren Gelder aus diesem Konto - Subadresse #%1$d - Öffentliche Subadresse #%1$d + Subadresse + Öffentliche Subadresse #%1$d: %2$s Sprache Benutze Systemsprache @@ -412,4 +412,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index f2c4815a..ee14b0cc 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -8,7 +8,7 @@ Μοιράσου Βοήθεια Λήψη - Μετονομασία … + Μετονομασία Αρχειοθέτησε Δημιουργία αντίγραφου ασφαλείας @@ -280,8 +280,8 @@ Added new account #%1$d Account # Send all confirmed funds in this account! - Subaddress #%1$d - Public Subaddress #%1$d + Subaddress + Public Subaddress #%1$d: %2$s Language Use System Language @@ -412,4 +412,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index a112287c..df997955 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -8,7 +8,7 @@ Kunhavigi Helpo Ricevi - Renomi … + Renomi Arkivo Sekurkopio Ŝanĝi pasfrazon @@ -287,8 +287,8 @@ Konto # Sendi ĉiujn konfirmitajn monsumojn al tiu konto! - Subadreso #%1$d - Publika subadreso #%1$d + Subadreso + Publika subadreso #%1$d: %2$s Lingvo Uzi la sistemlingvon @@ -412,4 +412,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ca9b3c8f..84f96d42 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -8,7 +8,7 @@ Compartir Ayuda Recibir - Renombrar … + Renombrar Archivar Copia de seguridad Cambiar contraseña @@ -269,8 +269,8 @@ # de cuenta ¡Enviar todos los fondos confirmados en esta cuenta! - Subdirecciones #%1$d - Subdirecciones Públicas #%1$d + Subdirección + Subdirecciones Públicas #%1$d: %2$s Lenguaje Usar Idioma del Sistema @@ -334,7 +334,7 @@ Probando el IP: %1$s … Por favor espera a que termine el escaneo Toca para seleccionar o agregar nodos - Agregar nodos manualmente o presiona para escanear + Agrega nodos manualmente o tira para buscar Escaneando la red… Mejores %1$d nodos marcados automáticamente Probar @@ -403,4 +403,14 @@ Por favor ingresa o escanea una dirección de Monero. ]]> + + Subdirecciones + Nombre de la subdirección + Demasiadas direcciones sin usar. ¡Usa alguna antes de crear más! + Demasiadas cuentas sin usar. ¡Usa alguna antes de crear más! + Transacciones recibidas en esta subdirección: + Aún no hay transacciones recibidas en esta subdirección. + Elige una subdirección + Presiona largo para ver detalles + diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index ad8e6cc1..02e50462 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -8,7 +8,7 @@ Jaga Abi Küsi raha - Nimeta ümber … + Nimeta ümber Arhiveeri Tagavarakoopia Vaheta parooli @@ -282,8 +282,8 @@ Konto # Saada kõik selle konto kinnitatud vahendid! - Alamaadress #%1$d - Avalik alamaadress #%1$d + Alamaadress + Avalik alamaadress #%1$d: %2$s Keel Kasuta süsteemi keelt @@ -410,4 +410,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 563431a8..7e941929 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -8,7 +8,7 @@ Partage Aide Recevoir - Renommer … + Renommer Archiver Sauvegarder Modifier Phrase secrète @@ -283,8 +283,8 @@ Compte # Envoyer tous les fonds confirmés sur ce compte ! - Sous-adresse #%1$d - Sous-adresse publique #%1$d + Sous-adresse + Sous-adresse publique #%1$d: %2$s Restaurer depuis Ledger Nano S @@ -416,4 +416,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 429404b9..e9966d62 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -8,7 +8,7 @@ Megosztás Segítség Fogadás - Átnevezés… + Átnevezés Archiválás Biztonsági mentés Jelszómódosítás @@ -280,8 +280,8 @@ Számla # Teljes megerősített egyenleg küldése! - Alcím #%1$d - Nyilvános alcím #%1$d + Alcím + Nyilvános alcím #%1$d: %2$s Nyelv Rendszernyelv használata @@ -414,4 +414,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 92d9060b..3ff7aa66 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -8,7 +8,7 @@ Condividi Aiuto Ricevi - Rinomina … + Rinomina Archivia Backup Cambia passphrase @@ -282,8 +282,8 @@ Account # Manda tutti i fondi confermati in questo account! - Sottoindirizzo #%1$d - Sottoindirizzo pubblico #%1$d + Sottoindirizzo + Sottoindirizzo pubblico #%1$d: %2$s Lingua Usa lingua di sistema @@ -415,4 +415,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index fec82efe..ec6b209d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -8,7 +8,7 @@ 共有 ヘルプ 受取り - 名前の変更 … + 名前の変更 アーカイブ バックアップ パスフレーズの変更 @@ -287,8 +287,8 @@ アカウント # このアカウントの承認された資産をすべて送金する! - サブアドレス#%1$d - 公開サブアドレス #%1$d + サブアドレス + 公開サブアドレス #%1$d: %2$s 言語 システムの言語を使う @@ -415,4 +415,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index bc9ba418..40bff1cd 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -8,7 +8,7 @@ Del Hjelp Motta - Gi nytt navn … + Gi nytt navn Arkiver Backup [Forandre passord] @@ -280,8 +280,8 @@ Account # Send all confirmed funds in this account! - Subaddress #%1$d - Public Subaddress #%1$d + Subaddress + Public Subaddress #%1$d: %2$s Language Use System Language @@ -412,4 +412,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b903a592..119bfea0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -8,7 +8,7 @@ Delen Help Ontvangen - Hernoemen… + Hernoemen Archiveren Back-up Wachtzin wijzigen @@ -277,8 +277,8 @@ Accountnr. Al het bevestigde geld in dit account verzenden! - Subadres #%1$d - Openbaar subadres #%1$d + Subadres + Openbaar subadres #%1$d: %2$s Taal Systeemtaal gebruiken @@ -412,4 +412,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 7e42d762..4fb7b1e0 100755 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -8,7 +8,7 @@ Compartilhar Ajuda Receber - Renomear … + Renomear Arquivar Backup Alterar Senha @@ -279,8 +279,8 @@ Conta # Enviar todo o saldo disponível nesta conta! - Subendereço #%1$d - Subendereço público #%1$d + Subendereço + Subendereço público #%1$d: %2$s Idioma Usar o idioma do sistema @@ -404,4 +404,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b6cc1d42..7fc18b42 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -8,7 +8,7 @@ Partilhar Ajuda Receber - Renomear … + Renomear Arquivar Cópia de segurança Alterar palavra passe @@ -282,8 +282,8 @@ Conta # Enviar todos os fundos confirmados para esta conta! - Subaddress #%1$d - Subaddress Publico #%1$d + Subaddress + Subaddress Publico #%1$d: %2$s Linguagem Usar linguagem de sistema @@ -416,4 +416,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 5d2861cb..334aa8fe 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -8,7 +8,7 @@ Împărtășește Ajutor Primește - Redenumește … + Redenumește Arhivă Backup @@ -280,8 +280,8 @@ Added new account #%1$d Account # Send all confirmed funds in this account! - Subaddress #%1$d - Public Subaddress #%1$d + Subaddress + Public Subaddress #%1$d: %2$s Language Use System Language @@ -412,4 +412,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 0d97e893..9b68e19d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -8,7 +8,7 @@ Поделиться Помощь Получить - Переименовать … + Переименовать Архив Резервная копия Изменить пароль @@ -281,8 +281,8 @@ Учетная запись # Отправить все средства на этот счет! - Субадрес #%1$d - Публичный субадрес #%1$d + Субадрес + Публичный субадрес #%1$d: %2$s Язык Использовать язык системы @@ -416,4 +416,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 86e720cb..b34b33b6 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -8,7 +8,7 @@ Zdieľaj Pomoc Prijať - Premenovať … + Premenovať Archivovať Záloha Zmena Hesla @@ -278,8 +278,8 @@ Účet # Pošli všetky potvrdené prostriedky na tomto účte! - Subadresa #%1$d - Verejná Subadresa #%1$d + Subadresa + Verejná Subadresa #%1$d: %2$s Jazyk Použi jazyk systému @@ -413,4 +413,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index ad42e37b..5abd7dae 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -8,7 +8,7 @@ Podeli Pomoć Primi - Preimenuj … + Preimenuj Arhiva Bekap Promeni lozinku @@ -288,8 +288,8 @@ Račun # Pošalji sva potvrđena sredstva na ovaj račun! - Subadresa #%1$d - Javna subadresa #%1$d + Subadresa + Javna subadresa #%1$d: %2$s Jezik Koristi sistemski jezik @@ -411,4 +411,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index d11cfcbc..7d760077 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -8,7 +8,7 @@ Dela Hjälp Ta emot - Byt namn … + Byt namn Arkivera Säkerhetskopiera Ändra lösenfras @@ -271,8 +271,8 @@ Konto # Skicka alla bekräftade pengar i detta konto! - Underadress #%1$d - Publik underadress #%1$d + Underadress + Publik underadress #%1$d: %2$s Språk Använd systemets språk @@ -404,4 +404,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index bf66f77c..8f1de6b4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -8,7 +8,7 @@ Поділитися Допомога Отримати - Перейменувати … + Перейменувати Архів Резервна копія Змінити пароль @@ -281,8 +281,8 @@ Обліковий запис # Надіслати всі кошти на цей рахунок! - Субадрес #%1$d - Публічна субадреса #%1$d + Субадрес + Публічна субадреса #%1$d: %2$s Мова Використовувати мову системи @@ -416,4 +416,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 622c2a96..4bbd57a6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -6,7 +6,7 @@ 分享 帮助 收款 - 重命名 … + 重命名 存档 备份 修改密码 @@ -224,8 +224,8 @@ 添加新账户 #%1$d 账户 # 发送这个账户中所有已确认的资金! - 子地址 #%1$d - 公开子地址#%1$d + 子地址 + 公开子地址#%1$d: %2$s 语言 使用系统默认语言 通过Ledger Nano S恢复 @@ -336,4 +336,14 @@ Please enter or scan a Monero address. ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a79ef679..d3b717cd 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -8,7 +8,7 @@ 分享 幫助 收款 - 重新命名 … + 重新命名 封存 備份 更改密碼 @@ -279,8 +279,8 @@ 帳戶 # 發送這個帳戶的所有已確認款項! - 子地址 #%1$d - 公開子地址 #%1$d + 子地址 + 公開子地址 #%1$d: %2$s 語言 使用系統語言 @@ -411,4 +411,14 @@ 請輸入或掃描一個 Monero 地址。 ]]> + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66c2261a..acc9f129 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - +]> monerujo Wallet @@ -9,7 +9,7 @@ Share Help Receive - Rename … + Rename Archive Backup Change Passphrase @@ -325,15 +325,15 @@ Restore wallet 25 word seed %1$s XMR - (%1$d, %2$d) + (%1$d, %2$d)  <span style=\"background-color: #%3$s; color: #%4$s;\"> %5$s </span> Create Account Added new account #%1$d Account # Send all confirmed funds in this account! - Subaddress #%1$d - Public Subaddress #%1$d + Subaddress + Public Subaddress #%1$d: %2$s Language Use System Language @@ -474,9 +474,25 @@ tx_transition_%1$s tx_transition - receive_transition receive_btn_transition - send_transition + receive_transition send_btn_transition + send_transition + subaddress_transition_%1$d + subaddress_transition + subaddress_txinfo_transition + + <span style=\"background-color: #%1$s; color: #%2$s;\"> %3$s </span>%4$s + <span style=\"background-color: #%1$s; color: #%2$s;\"> %3$s </span>  %4$s + #%1$d: %2$s + + Subaddresses + Subaddress Name + Too many unused addresses - use some to enable creating more! + Too many unused accounts - use some to enable creating more! + Transactions for this subaddress: + No transactions for this subaddress yet + Select a subaddress + Long-press for details diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 27bd042c..f254d698 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -193,6 +193,11 @@ 18sp + + diff --git a/build.gradle b/build.gradle index 0de97164..cdfb05ff 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.1.3' } }