1
mirror of https://github.com/m2049r/xmrwallet synced 2025-09-03 08:23:04 +02:00

Compare commits

...

14 Commits

Author SHA1 Message Date
m2049r
8170f823ab bump version 2021-04-19 18:41:09 +02:00
m2049r
38c0ead45c random fixes (#747) 2021-04-19 17:39:12 +02:00
m2049r
1d027c1694 fix translation 2021-04-18 19:39:00 +02:00
Vlad
45dc21fbf7 Update Romanian translation (#739)
Co-authored-by: m2049r <m2049r@monerujo.io>
2021-04-18 19:19:42 +02:00
netrik182
d4f4de234a pt-br translation of new strings (#735) 2021-04-18 19:17:29 +02:00
m2049r
394d5471e3 show total incoming amount (#745) 2021-04-18 19:16:52 +02:00
m2049r
beb098adb3 build version 2021-04-18 17:38:28 +02:00
m2049r
fda370d35b bump version 2021-04-18 16:57:58 +02:00
m2049r
99681e1bbb fix permissions Q needs to read our old wallet files (#744) 2021-04-18 16:55:14 +02:00
m2049r
21f44380b1 remove persmissions 2021-04-18 13:29:16 +02:00
m2049r
f4c1af1bb8 Some refactoring (#742)
* configure enabling of shift & exchange
* refactor qr logo code
* fix rename wallet
2021-04-17 21:03:32 +02:00
m2049r
c002b81ebd use default mixin 2021-04-15 19:10:56 +02:00
m2049r
d2f07ba3b6 migrate to scoped storage and API 30 (#741) 2021-04-15 19:10:01 +02:00
m2049r
24fc27b09e show new incoming tx in subaddress details (#734) 2021-03-23 19:05:30 +01:00
57 changed files with 1423 additions and 1099 deletions

View File

@@ -1,14 +1,14 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
compileSdkVersion 30
buildToolsVersion '29.0.3'
defaultConfig {
applicationId "com.m2049r.xmrwallet"
minSdkVersion 21
targetSdkVersion 29
versionCode 800
versionName "1.18.0 'ChAdOx1'"
targetSdkVersion 30
versionCode 1002
versionName "2.0.2 'Puginarug'"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
@@ -123,7 +123,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

View File

@@ -4,16 +4,14 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:requestLegacyExternalStorage="true"
android:preserveLegacyExternalStorage="true"
android:name=".XmrWalletApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,6 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -62,8 +61,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
private WalletInfoAdapter adapter;
private List<WalletManager.WalletInfo> walletList = new ArrayList<>();
private List<WalletManager.WalletInfo> displayedList = new ArrayList<>();
private final List<WalletManager.WalletInfo> walletList = new ArrayList<>();
private View tvGuntherSays;
private ImageView ivGunther;
@@ -86,7 +84,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
void onWalletBackup(String name);
void onWalletArchive(String walletName);
void onWalletRestore();
void onWalletDelete(String walletName);
void onAddWallet(String type);
@@ -110,7 +110,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Listener) {
this.activityCallback = (Listener) context;
@@ -200,12 +200,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
// Wallet touched
@Override
public void onInteraction(final View view, final WalletManager.WalletInfo infoItem) {
String addressPrefix = WalletManager.getInstance().addressPrefix();
if (addressPrefix.indexOf(infoItem.address.charAt(0)) < 0) {
Toast.makeText(getActivity(), getString(R.string.prompt_wrong_net), Toast.LENGTH_LONG).show();
return;
}
openWallet(infoItem.name, false);
openWallet(infoItem.getName(), false);
}
private void openWallet(String name, boolean streetmode) {
@@ -214,48 +209,32 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
@Override
public boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo listItem) {
switch (item.getItemId()) {
case R.id.action_streetmode:
openWallet(listItem.name, true);
break;
case R.id.action_info:
showInfo(listItem.name);
break;
case R.id.action_rename:
activityCallback.onWalletRename(listItem.name);
break;
case R.id.action_backup:
activityCallback.onWalletBackup(listItem.name);
break;
case R.id.action_archive:
activityCallback.onWalletArchive(listItem.name);
break;
default:
return super.onContextItemSelected(item);
final int id = item.getItemId();
if (id == R.id.action_streetmode) {
openWallet(listItem.getName(), true);
} else if (id == R.id.action_info) {
showInfo(listItem.getName());
} else if (id == R.id.action_rename) {
activityCallback.onWalletRename(listItem.getName());
} else if (id == R.id.action_backup) {
activityCallback.onWalletBackup(listItem.getName());
} else if (id == R.id.action_archive) {
activityCallback.onWalletDelete(listItem.getName());
} else {
return super.onContextItemSelected(item);
}
return true;
}
private void filterList() {
displayedList.clear();
String addressPrefix = WalletManager.getInstance().addressPrefix();
for (WalletManager.WalletInfo s : walletList) {
if (addressPrefix.indexOf(s.address.charAt(0)) >= 0) displayedList.add(s);
}
}
public void loadList() {
Timber.d("loadList()");
WalletManager mgr = WalletManager.getInstance();
List<WalletManager.WalletInfo> walletInfos =
mgr.findWallets(activityCallback.getStorageRoot());
walletList.clear();
walletList.addAll(walletInfos);
filterList();
adapter.setInfos(displayedList);
walletList.addAll(mgr.findWallets(activityCallback.getStorageRoot()));
adapter.setInfos(walletList);
// deal with Gunther & FAB animation
if (displayedList.isEmpty()) {
if (walletList.isEmpty()) {
fab.startAnimation(fab_pulse);
if (ivGunther.getDrawable() == null) {
ivGunther.setImageResource(R.drawable.ic_emptygunther);
@@ -274,7 +253,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
.getAll().keySet();
for (WalletManager.WalletInfo s : walletList) {
removedWallets.remove(s.name);
removedWallets.remove(s.getName());
}
for (String name : removedWallets) {
KeyStoreHelper.removeWalletUserPass(getActivity(), name);
@@ -292,7 +271,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.list_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@@ -362,37 +341,29 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
@Override
public void onClick(View v) {
int id = v.getId();
final int id = v.getId();
Timber.d("onClick %d/%d", id, R.id.fabLedger);
switch (id) {
case R.id.fab:
animateFAB();
break;
case R.id.fabNew:
fabScreen.setVisibility(View.INVISIBLE);
isFabOpen = false;
activityCallback.onAddWallet(GenerateFragment.TYPE_NEW);
break;
case R.id.fabView:
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_VIEWONLY);
break;
case R.id.fabKey:
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_KEY);
break;
case R.id.fabSeed:
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_SEED);
break;
case R.id.fabLedger:
Timber.d("FAB_LEDGER");
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
break;
case R.id.fabScreen:
animateFAB();
break;
if (id == R.id.fab) {
animateFAB();
} else if (id == R.id.fabNew) {
fabScreen.setVisibility(View.INVISIBLE);
isFabOpen = false;
activityCallback.onAddWallet(GenerateFragment.TYPE_NEW);
} else if (id == R.id.fabView) {
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_VIEWONLY);
} else if (id == R.id.fabKey) {
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_KEY);
} else if (id == R.id.fabSeed) {
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_SEED);
} else if (id == R.id.fabLedger) {
Timber.d("FAB_LEDGER");
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
} else if (id == R.id.fabScreen) {
animateFAB();
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2021 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 com.m2049r.xmrwallet.model.Wallet;
public interface OnBlockUpdateListener {
void onBlockUpdate(final Wallet wallet);
}

View File

@@ -385,24 +385,18 @@ public class ReceiveFragment extends Fragment {
}
private Bitmap addLogo(Bitmap qrBitmap) {
// addume logo & qrcode are both square
Bitmap logo = getMoneroLogo();
int qrWidth = qrBitmap.getWidth();
int qrHeight = qrBitmap.getHeight();
int logoWidth = logo.getWidth();
int logoHeight = logo.getHeight();
final int qrSize = qrBitmap.getWidth();
final int logoSize = logo.getWidth();
Bitmap logoBitmap = Bitmap.createBitmap(qrWidth, qrHeight, Bitmap.Config.ARGB_8888);
Bitmap logoBitmap = Bitmap.createBitmap(qrSize, qrSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(logoBitmap);
canvas.drawBitmap(qrBitmap, 0, 0, null);
canvas.save();
// figure out how to scale the logo
float scaleSize = 1.0f;
while ((logoWidth / scaleSize) > (qrWidth / 5.) || (logoHeight / scaleSize) > (qrHeight / 5.)) {
scaleSize *= 2;
}
float sx = 1.0f / scaleSize;
canvas.scale(sx, sx, qrWidth / 2f, qrHeight / 2f);
canvas.drawBitmap(logo, (qrWidth - logoWidth) / 2f, (qrHeight - logoHeight) / 2f, null);
final float sx = 0.2f * qrSize / logoSize;
canvas.scale(sx, sx, qrSize / 2f, qrSize / 2f);
canvas.drawBitmap(logo, (qrSize - logoSize) / 2f, (qrSize - logoSize) / 2f, null);
canvas.restore();
return logoBitmap;
}

View File

@@ -48,7 +48,7 @@ import lombok.RequiredArgsConstructor;
import timber.log.Timber;
public class SubaddressFragment extends Fragment implements SubaddressInfoAdapter.OnInteractionListener,
View.OnClickListener {
View.OnClickListener, OnBlockUpdateListener {
static public final String KEY_MODE = "mode";
static public final String MODE_MANAGER = "manager";
@@ -169,6 +169,11 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
adapter.setInfos(list);
}
@Override
public void onBlockUpdate(Wallet wallet) {
loadList();
}
@Override
public void onClick(View v) {
int id = v.getId();

View File

@@ -45,8 +45,8 @@ import java.util.List;
import timber.log.Timber;
// TODO: live update - i.e. use onRefreshed() somehow
public class SubaddressInfoFragment extends Fragment implements TransactionInfoAdapter.OnInteractionListener {
public class SubaddressInfoFragment extends Fragment
implements TransactionInfoAdapter.OnInteractionListener, OnBlockUpdateListener {
private TransactionInfoAdapter adapter;
private Subaddress subaddress;
@@ -123,6 +123,11 @@ public class SubaddressInfoFragment extends Fragment implements TransactionInfoA
tvTxLabel.setText(R.string.subaddress_tx_label);
}
@Override
public void onBlockUpdate(Wallet wallet) {
onRefreshed(wallet);
}
// Callbacks from TransactionInfoAdapter
@Override
public void onInteraction(final View view, final TransactionInfo infoItem) {

View File

@@ -141,6 +141,14 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
return synced;
}
private WalletFragment getWalletFragment() {
return (WalletFragment) getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName());
}
private Fragment getCurrentFragment() {
return getSupportFragmentManager().findFragmentById(R.id.fragment_container);
}
@Override
public boolean isStreetMode() {
return streetMode > 0;
@@ -152,8 +160,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
} else {
streetMode = 0;
}
final WalletFragment walletFragment = (WalletFragment)
getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName());
final WalletFragment walletFragment = getWalletFragment();
if (walletFragment != null) walletFragment.resetDismissedTransactions();
forceUpdate();
runOnUiThread(() -> {
@@ -220,8 +227,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
private void onWalletRescan() {
try {
final WalletFragment walletFragment = (WalletFragment)
getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName());
final WalletFragment walletFragment = getWalletFragment();
getWallet().rescanBlockchainAsync();
synced = false;
walletFragment.unsync();
@@ -336,8 +342,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
public void onWalletChangePassword() {
try {
GenerateReviewFragment detailsFragment = (GenerateReviewFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
GenerateReviewFragment detailsFragment = (GenerateReviewFragment) getCurrentFragment();
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
if (dialog != null) {
Helper.showKeyboard(dialog);
@@ -571,8 +576,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
runOnUiThread(this::updateAccountsList);
}
try {
final WalletFragment walletFragment = (WalletFragment)
getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName());
final WalletFragment walletFragment = getWalletFragment();
if (wallet.isSynchronized()) {
Timber.d("onRefreshed() synced");
releaseWakeLock(RELEASE_WAKE_LOCK_DELAY); // the idea is to stay awake until synced
@@ -583,7 +587,10 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
runOnUiThread(walletFragment::onSynced);
}
}
runOnUiThread(() -> walletFragment.onRefreshed(wallet, full));
runOnUiThread(() -> {
walletFragment.onRefreshed(wallet, full);
updateCurrentFragment(wallet);
});
return true;
} catch (ClassCastException ex) {
// not in wallet fragment (probably send monero)
@@ -593,6 +600,13 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
return false;
}
private void updateCurrentFragment(final Wallet wallet) {
final Fragment fragment = getCurrentFragment();
if (fragment instanceof OnBlockUpdateListener) {
((OnBlockUpdateListener) fragment).onBlockUpdate(wallet);
}
}
@Override
public void onWalletStored(final boolean success) {
runOnUiThread(() -> {
@@ -635,8 +649,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
if (requestStreetMode) onEnableStreetMode();
final WalletFragment walletFragment = (WalletFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
final WalletFragment walletFragment = getWalletFragment();
runOnUiThread(() -> {
updateAccountsHeader();
if (walletFragment != null) {
@@ -649,8 +662,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
@Override
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
try {
final SendFragment sendFragment = (SendFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
final SendFragment sendFragment = (SendFragment) getCurrentFragment();
runOnUiThread(() -> {
dismissProgressDialog();
PendingTransaction.Status status = pendingTransaction.getStatus();
@@ -673,8 +685,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
@Override
public void onSendTransactionFailed(final String error) {
try {
final SendFragment sendFragment = (SendFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
final SendFragment sendFragment = (SendFragment) getCurrentFragment();
runOnUiThread(() -> sendFragment.onSendTransactionFailed(error));
} catch (ClassCastException ex) {
// not in spend fragment
@@ -685,8 +696,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
@Override
public void onTransactionSent(final String txId) {
try {
final SendFragment sendFragment = (SendFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
final SendFragment sendFragment = (SendFragment) getCurrentFragment();
runOnUiThread(() -> sendFragment.onTransactionSent(txId));
} catch (ClassCastException ex) {
// not in spend fragment
@@ -697,8 +707,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
@Override
public void onProgress(final String text) {
try {
final WalletFragment walletFragment = (WalletFragment)
getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName());
final WalletFragment walletFragment = getWalletFragment();
runOnUiThread(new Runnable() {
public void run() {
walletFragment.setProgress(text);
@@ -715,8 +724,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
public void onProgress(final int n) {
runOnUiThread(() -> {
try {
WalletFragment walletFragment = (WalletFragment)
getSupportFragmentManager().findFragmentByTag(WalletFragment.class.getName());
WalletFragment walletFragment = getWalletFragment();
if (walletFragment != null)
walletFragment.setProgress(n);
} catch (ClassCastException ex) {
@@ -857,8 +865,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
void onShareTxInfo() {
try {
TxFragment fragment = (TxFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
TxFragment fragment = (TxFragment) getCurrentFragment();
fragment.shareTxInfo();
} catch (ClassCastException ex) {
// not in wallet fragment
@@ -968,7 +975,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
drawer.closeDrawer(GravityCompat.START);
return;
}
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
final Fragment fragment = getCurrentFragment();
if (fragment instanceof OnBackPressedListener) {
if (!((OnBackPressedListener) fragment).onBackPressed()) {
super.onBackPressed();

View File

@@ -126,7 +126,8 @@ public class WalletFragment extends Fragment
sCurrency = view.findViewById(R.id.sCurrency);
List<String> currencies = new ArrayList<>();
currencies.add(Helper.BASE_CRYPTO);
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
if (Helper.SHOW_EXCHANGERATES)
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(Objects.requireNonNull(getContext()), R.layout.item_spinner_balance, currencies);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
sCurrency.setAdapter(spinnerAdapter);
@@ -363,7 +364,6 @@ public class WalletFragment extends Fragment
accountIndex = wallet.getAccountIndex();
txlist.scrollToPosition(0);
}
}
updateStatus(wallet);
}

View File

@@ -18,12 +18,13 @@ package com.m2049r.xmrwallet.data;
import java.util.regex.Pattern;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@AllArgsConstructor
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
public class Subaddress implements Comparable<Subaddress> {
@@ -35,6 +36,9 @@ public class Subaddress implements Comparable<Subaddress> {
final private String address;
@Getter
private final String label;
@Getter
@Setter
private long amount;
@Override
public int compareTo(Subaddress another) { // newer is <

View File

@@ -117,32 +117,34 @@ public class SendAddressWizardFragment extends SendWizardFragment {
View view = inflater.inflate(R.layout.fragment_send_address, container, false);
tvXmrTo = view.findViewById(R.id.tvXmrTo);
ibCrypto = new HashMap<>();
for (Crypto crypto : Crypto.values()) {
final ImageButton button = view.findViewById(crypto.getButtonId());
ibCrypto.put(crypto, button);
button.setOnClickListener(v -> {
if (possibleCryptos.contains(crypto)) {
selectedCrypto = crypto;
updateCryptoButtons(false);
} else {
// show help what to do:
if (button.getId() != R.id.ibXMR) {
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
final String symbol = getCryptoForButton(button).getSymbol();
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
tvXmrTo.setVisibility(View.VISIBLE);
if (Helper.ALLOW_SHIFT) {
tvXmrTo = view.findViewById(R.id.tvXmrTo);
ibCrypto = new HashMap<>();
for (Crypto crypto : Crypto.values()) {
final ImageButton button = view.findViewById(crypto.getButtonId());
ibCrypto.put(crypto, button);
button.setOnClickListener(v -> {
if (possibleCryptos.contains(crypto)) {
selectedCrypto = crypto;
updateCryptoButtons(false);
} else {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
tvXmrTo.setVisibility(View.VISIBLE);
// show help what to do:
if (button.getId() != R.id.ibXMR) {
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
final String symbol = getCryptoForButton(button).getSymbol();
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
tvXmrTo.setVisibility(View.VISIBLE);
} else {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
tvXmrTo.setVisibility(View.VISIBLE);
}
}
}
});
});
}
updateCryptoButtons(true);
} else {
view.findViewById(R.id.llExchange).setVisibility(View.GONE);
}
updateCryptoButtons(true);
etAddress = view.findViewById(R.id.etAddress);
etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etAddress.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
@@ -181,6 +183,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
selectedCrypto = Crypto.XMR;
sendListener.setMode(SendFragment.Mode.XMR);
}
if (!Helper.ALLOW_SHIFT) return;
if ((selectedCrypto == null) && isEthAddress(address)) {
Timber.d("isEthAddress");
possibleCryptos.add(Crypto.ETH);
@@ -285,6 +288,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
private void updateCryptoButtons(boolean noAddress) {
if (!Helper.ALLOW_SHIFT) return;
for (Crypto crypto : Crypto.values()) {
if (crypto == selectedCrypto) {
selectedCrypto(crypto);
@@ -447,7 +451,11 @@ public class SendAddressWizardFragment extends SendWizardFragment {
BarcodeData barcodeData = sendListener.getBarcodeData();
if (barcodeData != null) {
Timber.d("GOT DATA");
if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) {
Timber.d("BUT ONLY XMR SUPPORTED");
barcodeData = null;
sendListener.setBarcodeData(barcodeData);
}
if (barcodeData.address != null) {
etAddress.getEditText().setText(barcodeData.address);
possibleCryptos.clear();
@@ -458,7 +466,8 @@ public class SendAddressWizardFragment extends SendWizardFragment {
possibleCryptos.add(barcodeData.asset);
selectedCrypto = barcodeData.asset;
}
updateCryptoButtons(false);
if (Helper.ALLOW_SHIFT)
updateCryptoButtons(false);
if (checkAddress()) {
if (barcodeData.security == BarcodeData.Security.OA_NO_DNSSEC)
etAddress.setError(getString(R.string.send_address_no_dnssec));

View File

@@ -66,7 +66,7 @@ public class SendFragment extends Fragment
SendSuccessWizardFragment.Listener,
OnBackPressedListener, OnUriScannedListener {
final static public int MIXIN = 10;
final static public int MIXIN = 0;
private Listener activityCallback;

View File

@@ -28,6 +28,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Subaddress;
import com.m2049r.xmrwallet.util.Helper;
import java.util.ArrayList;
import java.util.Collections;
@@ -110,12 +111,14 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
final TextView tvName;
final TextView tvAddress;
final TextView tvAmount;
Subaddress item;
ViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tvName);
tvAddress = itemView.findViewById(R.id.tvAddress);
tvAmount = itemView.findViewById(R.id.tx_amount);
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
@@ -129,6 +132,12 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
item.getAddressIndex(), item.getSquashedAddress());
tvName.setText(label.isEmpty() ? address : label);
tvAddress.setText(address);
final long amount = item.getAmount();
if (amount > 0)
tvAmount.setText(context.getString(R.string.tx_list_amount_positive,
Helper.getDisplayAmount(amount, Helper.DISPLAY_DIGITS_INFO)));
else
tvAmount.setText("");
}
@Override

View File

@@ -74,7 +74,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).name.equals(mNewList.get(newItemPosition).name);
return mOldList.get(oldItemPosition).getName().equals(mNewList.get(newItemPosition).getName());
}
@Override
@@ -86,9 +86,9 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_wallet, parent, false);
return new ViewHolder(view);
return new ViewHolder(
LayoutInflater.from(parent.getContext()).inflate(R.layout.item_wallet, parent, false)
);
}
@Override
@@ -122,7 +122,6 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView tvName;
final TextView tvAddress;
final ImageButton ibOptions;
WalletManager.WalletInfo infoItem;
boolean popupOpen = false;
@@ -130,7 +129,6 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
ViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tvName);
tvAddress = itemView.findViewById(R.id.tvAddress);
ibOptions = itemView.findViewById(R.id.ibOptions);
ibOptions.setOnClickListener(view -> {
if (popupOpen) return;
@@ -160,8 +158,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
void bind(int position) {
infoItem = infoItems.get(position);
tvName.setText(infoItem.name);
tvAddress.setText(infoItem.address.substring(0, 16) + "...");
tvName.setText(infoItem.getName());
}
@Override

View File

@@ -173,7 +173,16 @@ public class Wallet {
}
public Subaddress getSubaddressObject(int subAddressIndex) {
return getSubaddressObject(accountIndex, subAddressIndex);
Subaddress subaddress = getSubaddressObject(accountIndex, subAddressIndex);
long amount = 0;
for (TransactionInfo info : getHistory().getAll()) {
if ((info.addressIndex == subAddressIndex)
&& (info.direction == TransactionInfo.Direction.Direction_In)) {
amount += info.amount;
}
}
subaddress.setAmount(amount);
return subaddress;
}
public native String getPath();

View File

@@ -21,15 +21,13 @@ import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.ledger.Ledger;
import com.m2049r.xmrwallet.util.RestoreHeight;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import lombok.Getter;
import timber.log.Timber;
public class WalletManager {
@@ -103,7 +101,8 @@ public class WalletManager {
wallet.setRestoreHeight(restoreHeight);
Timber.d("Changed Restore Height from %d to %d", oldHeight, wallet.getRestoreHeight());
wallet.setPassword(password); // this rewrites the keys file (which contains the restore height)
}
} else
Timber.e(wallet.getStatus().toString());
return wallet;
}
@@ -210,46 +209,22 @@ public class WalletManager {
//public native List<String> findWallets(String path); // this does not work - some error in boost
public class WalletInfo implements Comparable<WalletInfo> {
public File path;
public String name;
public String address;
@Getter
final private File path;
@Getter
final private String name;
public WalletInfo(File wallet) {
path = wallet.getParentFile();
name = wallet.getName();
}
@Override
public int compareTo(WalletInfo another) {
int n = name.toLowerCase().compareTo(another.name.toLowerCase());
if (n != 0) {
return n;
} else { // wallet names are the same
return address.compareTo(another.address);
}
return name.toLowerCase().compareTo(another.name.toLowerCase());
}
}
public WalletInfo getWalletInfo(File wallet) {
WalletInfo info = new WalletInfo();
info.path = wallet.getParentFile();
info.name = wallet.getName();
File addressFile = new File(info.path, info.name + ".address.txt");
//Timber.d(addressFile.getAbsolutePath());
info.address = "??????";
BufferedReader addressReader = null;
try {
addressReader = new BufferedReader(new FileReader(addressFile));
info.address = addressReader.readLine();
} catch (IOException ex) {
Timber.d(ex.getLocalizedMessage());
} finally {
if (addressReader != null) {
try {
addressReader.close();
} catch (IOException ex) {
// that's just too bad
}
}
}
return info;
}
public List<WalletInfo> findWallets(File path) {
List<WalletInfo> wallets = new ArrayList<>();
Timber.d("Scanning: %s", path.getAbsolutePath());
@@ -261,7 +236,7 @@ public class WalletManager {
for (int i = 0; i < found.length; i++) {
String filename = found[i].getName();
File f = new File(found[i].getParent(), filename.substring(0, filename.length() - 5)); // 5 is length of ".keys"+1
wallets.add(getWalletInfo(f));
wallets.add(new WalletInfo(f));
}
return wallets;
}

View File

@@ -34,7 +34,6 @@ import android.hardware.fingerprint.FingerprintManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.StrictMode;
import android.system.ErrnoException;
import android.system.Os;
@@ -58,6 +57,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.model.WalletManager;
import java.io.File;
@@ -77,16 +77,16 @@ import javax.net.ssl.HttpsURLConnection;
import timber.log.Timber;
public class Helper {
static private final String FLAVOR_SUFFIX =
(BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR)
+ (BuildConfig.DEBUG ? "-debug" : "");
static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass";
static public final String BASE_CRYPTO = "XMR";
static public final String BASE_CRYPTO = Crypto.XMR.getSymbol();
static public final int XMR_DECIMALS = 12;
static private final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX;
static private final String HOME_DIR = "monero" + FLAVOR_SUFFIX;
static public final boolean SHOW_EXCHANGERATES = true;
static public final boolean ALLOW_SHIFT = true;
static private final String WALLET_DIR = "wallets";
static private final String MONERO_DIR = "monero";
static public int DISPLAY_DIGITS_INFO = 5;
@@ -95,12 +95,7 @@ public class Helper {
}
static public File getStorage(Context context, String folderName) {
if (!isExternalStorageWritable()) {
String msg = context.getString(R.string.message_strorage_not_writable);
Timber.e(msg);
throw new IllegalStateException(msg);
}
File dir = new File(Environment.getExternalStorageDirectory(), folderName);
File dir = new File(context.getFilesDir(), folderName);
if (!dir.exists()) {
Timber.i("Creating %s", dir.getAbsolutePath());
dir.mkdirs(); // try to make it
@@ -113,24 +108,6 @@ public class Helper {
return dir;
}
static public final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
static public boolean getWritePermission(Activity context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
if (context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_DENIED) {
Timber.w("Permission denied to WRITE_EXTERNAL_STORAGE - requesting it");
String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
context.requestPermissions(permissions, PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
return false;
} else {
return true;
}
} else {
return true;
}
}
static public final int PERMISSIONS_REQUEST_CAMERA = 7;
static public boolean getCameraPermission(Activity context) {
@@ -156,12 +133,6 @@ public class Helper {
return f;
}
/* Checks if external storage is available for read and write */
private static boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
return Environment.MEDIA_MOUNTED.equals(state);
}
static public void showKeyboard(Activity act) {
InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE);
final View focus = act.getCurrentFocus();
@@ -189,11 +160,11 @@ public class Helper {
}
static public BigDecimal getDecimalAmount(long amount) {
return new BigDecimal(amount).scaleByPowerOfTen(-12);
return new BigDecimal(amount).scaleByPowerOfTen(-XMR_DECIMALS);
}
static public String getDisplayAmount(long amount) {
return getDisplayAmount(amount, 12);
return getDisplayAmount(amount, XMR_DECIMALS);
}
static public String getDisplayAmount(long amount, int maxDecimals) {
@@ -225,7 +196,7 @@ public class Helper {
static public String getDisplayAmount(double amount) {
// a Java bug does not strip zeros properly if the value is 0
BigDecimal d = new BigDecimal(amount)
.setScale(12, BigDecimal.ROUND_HALF_UP)
.setScale(XMR_DECIMALS, BigDecimal.ROUND_HALF_UP)
.stripTrailingZeros();
if (d.scale() < 1)
d = d.setScale(1, BigDecimal.ROUND_UNNECESSARY);
@@ -339,7 +310,7 @@ public class Helper {
static public void setMoneroHome(Context context) {
try {
String home = getStorage(context, HOME_DIR).getAbsolutePath();
String home = getStorage(context, MONERO_DIR).getAbsolutePath();
Os.setenv("HOME", home, true);
} catch (ErrnoException ex) {
throw new IllegalStateException(ex);
@@ -355,7 +326,7 @@ public class Helper {
// TODO make the log levels refer to the WalletManagerFactory::LogLevel enum ?
static public void initLogger(Context context, int level) {
String home = getStorage(context, HOME_DIR).getAbsolutePath();
String home = getStorage(context, MONERO_DIR).getAbsolutePath();
WalletManager.initLogger(home + "/monerujo", "monerujo.log");
if (level >= WalletManager.LOGLEVEL_SILENT)
WalletManager.setLogLevel(level);

View File

@@ -24,9 +24,10 @@ import android.os.Build;
import android.security.KeyPairGeneratorSpec;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import androidx.annotation.NonNull;
import android.util.Base64;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
@@ -167,6 +168,11 @@ public class KeyStoreHelper {
.remove(wallet).apply();
}
public static void copyWalletUserPass(Context context, String srcWallet, String dstWallet) throws BrokenPasswordStoreException {
final String pass = loadWalletUserPass(context, srcWallet);
saveWalletUserPass(context, dstWallet, pass);
}
/**
* Creates a public and private key and stores it using the Android Key
* Store, so that only this application will be able to access the keys.

View File

@@ -0,0 +1,171 @@
package com.m2049r.xmrwallet.util;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.preference.PreferenceManager;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.model.WalletManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
@RequiredArgsConstructor
public class LegacyStorageHelper {
final private File srcDir;
final private File dstDir;
static public void migrateWallets(Context context) {
try {
if (isStorageMigrated(context)) return;
if (!hasReadPermission(context)) {
// nothing to migrate, so don't try again
setStorageMigrated(context);
return;
}
final File oldRoot = getWalletRoot();
if (!oldRoot.exists()) {
// nothing to migrate, so don't try again
setStorageMigrated(context);
return;
}
final File newRoot = Helper.getWalletRoot(context);
(new LegacyStorageHelper(oldRoot, newRoot)).migrate();
setStorageMigrated(context); // done it once - don't try again
} catch (IllegalStateException ex) {
Timber.d(ex);
// nothing we can do here
}
}
public void migrate() {
String addressPrefix = WalletManager.getInstance().addressPrefix();
File[] wallets = srcDir.listFiles((dir, filename) -> filename.endsWith(".keys"));
if (wallets == null) return;
for (File wallet : wallets) {
final String walletName = wallet.getName().substring(0, wallet.getName().length() - ".keys".length());
if (addressPrefix.indexOf(getAddress(walletName).charAt(0)) < 0) {
Timber.d("skipping %s", walletName);
continue;
}
try {
copy(walletName);
} catch (IOException ex) { // something failed - try to clean up
deleteDst(walletName);
}
}
}
// return "@" by default so we don't need to deal with null stuff
private String getAddress(String walletName) {
File addressFile = new File(srcDir, walletName + ".address.txt");
if (!addressFile.exists()) return "@";
try (BufferedReader addressReader = new BufferedReader(new FileReader(addressFile))) {
return addressReader.readLine();
} catch (IOException ex) {
Timber.d(ex.getLocalizedMessage());
}
return "@";
}
private void copy(String walletName) throws IOException {
final String dstName = getUniqueName(dstDir, walletName);
copyFile(new File(srcDir, walletName), new File(dstDir, dstName));
copyFile(new File(srcDir, walletName + ".keys"), new File(dstDir, dstName + ".keys"));
}
private void deleteDst(String walletName) {
// do our best, but if it fails, it fails
(new File(dstDir, walletName)).delete();
(new File(dstDir, walletName + ".keys")).delete();
}
private void copyFile(File src, File dst) throws IOException {
if (!src.exists()) return;
Timber.d("%s => %s", src.getAbsolutePath(), dst.getAbsolutePath());
try (FileChannel inChannel = new FileInputStream(src).getChannel();
FileChannel outChannel = new FileOutputStream(dst).getChannel()) {
inChannel.transferTo(0, inChannel.size(), outChannel);
}
}
private static boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
return Environment.MEDIA_MOUNTED.equals(state);
}
private static File getWalletRoot() {
if (!isExternalStorageWritable())
throw new IllegalStateException();
// wallet folder for legacy (pre-Q) installations
final String FLAVOR_SUFFIX =
(BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR)
+ (BuildConfig.DEBUG ? "-debug" : "");
final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX;
File dir = new File(Environment.getExternalStorageDirectory(), WALLET_DIR);
if (!dir.exists() || !dir.isDirectory())
throw new IllegalStateException();
return dir;
}
private static boolean hasReadPermission(Context context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
return context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_DENIED;
} else {
return true;
}
}
private static final Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$");
private static String getUniqueName(File root, String name) {
if (!(new File(root, name + ".keys")).exists()) // <name> does not exist => it's ok to use
return name;
File[] wallets = root.listFiles(
(dir, filename) -> {
Matcher m = WALLET_PATTERN.matcher(filename);
if (m.find())
return m.group(1).equals(name);
else return false;
});
if (wallets.length == 0) return name + " (1)";
int maxIndex = 0;
for (File wallet : wallets) {
try {
final Matcher m = WALLET_PATTERN.matcher(wallet.getName());
if (!m.find())
throw new IllegalStateException("this must match as it did before");
final int index = Integer.parseInt(m.group(2));
if (index > maxIndex) maxIndex = index;
} catch (NumberFormatException ex) {
// this cannot happen & we can ignore it if it does
}
}
return name + " (" + (maxIndex + 1) + ")";
}
private static final String MIGRATED_KEY = "migrated_legacy_storage";
public static boolean isStorageMigrated(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(MIGRATED_KEY, false);
}
public static void setStorageMigrated(Context context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(MIGRATED_KEY, true).apply();
}
}

View File

@@ -18,8 +18,6 @@ package com.m2049r.xmrwallet.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -27,8 +25,12 @@ import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.dialog.HelpFragment;
import com.m2049r.xmrwallet.ledger.Ledger;
import java.util.ArrayList;
import java.util.List;
@@ -44,18 +46,20 @@ public class Notice {
synchronized (Notice.class) {
if (notices != null) return;
notices = new ArrayList<>();
notices.add(
new Notice(NOTICE_SHOW_XMRTO_ENABLED_SEND,
R.string.info_xmrto_enabled,
R.string.help_xmrto,
1)
);
notices.add(
new Notice(NOTICE_SHOW_LEDGER,
R.string.info_ledger_enabled,
R.string.help_create_ledger,
1)
);
if (Helper.ALLOW_SHIFT)
notices.add(
new Notice(NOTICE_SHOW_XMRTO_ENABLED_SEND,
R.string.info_xmrto_enabled,
R.string.help_xmrto,
1)
);
if (Ledger.ENABLED)
notices.add(
new Notice(NOTICE_SHOW_LEDGER,
R.string.info_ledger_enabled,
R.string.help_create_ledger,
1)
);
}
}

View File

@@ -26,6 +26,8 @@ import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
public class RestoreHeight {
static final int DIFFICULTY_TARGET = 120; // seconds
static private RestoreHeight Singleton = null;
static public RestoreHeight getInstance() {
@@ -190,7 +192,7 @@ public class RestoreHeight {
} else {
long days = TimeUnit.DAYS.convert(query.getTimeInMillis() - prevTime,
TimeUnit.MILLISECONDS);
height = Math.round(prevBc + 1.0 * days * (24 * 60 / 2));
height = Math.round(prevBc + 1.0 * days * (24f * 60 * 60 / DIFFICULTY_TARGET));
}
return height;
}

View File

@@ -0,0 +1,64 @@
package com.m2049r.xmrwallet.util;
import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ZipBackup {
final private Context context;
final private String walletName;
private ZipOutputStream zip;
public void writeTo(Uri zipUri) throws IOException {
if (zip != null)
throw new IllegalStateException("zip already initialized");
try {
zip = new ZipOutputStream(context.getContentResolver().openOutputStream(zipUri));
final File walletRoot = Helper.getWalletRoot(context);
addFile(new File(walletRoot, walletName + ".keys"));
addFile(new File(walletRoot, walletName));
zip.close();
} finally {
if (zip != null) zip.close();
}
}
private void addFile(File file) throws IOException {
if (!file.exists()) return; // ignore missing files (e.g. the cache file might not exist)
ZipEntry entry = new ZipEntry(file.getName());
zip.putNextEntry(entry);
writeFile(file);
zip.closeEntry();
}
private void writeFile(File source) throws IOException {
try (InputStream is = new FileInputStream(source)) {
byte[] buffer = new byte[8192];
int length;
while ((length = is.read(buffer)) > 0) {
zip.write(buffer, 0, length);
}
}
}
private static final SimpleDateFormat DATETIME_FORMATTER =
new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss");
public String getBackupName() {
return walletName + " " + DATETIME_FORMATTER.format(new Date());
}
}

View File

@@ -0,0 +1,139 @@
package com.m2049r.xmrwallet.util;
import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
@RequiredArgsConstructor
public class ZipRestore {
final private Context context;
final private Uri zipUri;
private File walletRoot;
private ZipInputStream zip;
public boolean restore() throws IOException {
walletRoot = Helper.getWalletRoot(context);
String walletName = testArchive();
if (walletName == null) return false;
walletName = getUniqueName(walletName);
if (zip != null)
throw new IllegalStateException("zip already initialized");
try {
zip = new ZipInputStream(context.getContentResolver().openInputStream(zipUri));
for (ZipEntry entry = zip.getNextEntry(); entry != null; zip.closeEntry(), entry = zip.getNextEntry()) {
File destination;
final String name = entry.getName();
if (name.endsWith(".keys")) {
destination = new File(walletRoot, walletName + ".keys");
} else if (name.endsWith(".address.txt")) {
destination = new File(walletRoot, walletName + ".address.txt");
} else {
destination = new File(walletRoot, walletName);
}
writeFile(destination);
}
} finally {
if (zip != null) zip.close();
}
return true;
}
private void writeFile(File destination) throws IOException {
try (OutputStream os = new FileOutputStream(destination)) {
byte[] buffer = new byte[8192];
int length;
while ((length = zip.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
// test the archive to contain files we expect & return the name of the contained wallet or null
private String testArchive() {
String walletName = null;
boolean keys = false;
ZipInputStream zipStream = null;
try {
zipStream = new ZipInputStream(context.getContentResolver().openInputStream(zipUri));
for (ZipEntry entry = zipStream.getNextEntry(); entry != null;
zipStream.closeEntry(), entry = zipStream.getNextEntry()) {
if (entry.isDirectory())
return null;
final String name = entry.getName();
if ((new File(name)).getParentFile() != null)
return null;
if (walletName == null) {
if (name.endsWith(".keys")) {
walletName = name.substring(0, name.length() - ".keys".length());
keys = true; // we have they keys
} else if (name.endsWith(".address.txt")) {
walletName = name.substring(0, name.length() - ".address.txt".length());
} else {
walletName = name;
}
} else { // we have a wallet name
if (name.endsWith(".keys")) {
if (!name.equals(walletName + ".keys")) return null;
keys = true; // we have they keys
} else if (name.endsWith(".address.txt")) {
if (!name.equals(walletName + ".address.txt")) return null;
} else if (!name.equals(walletName)) return null;
}
}
} catch (IOException ex) {
return null;
} finally {
try {
if (zipStream != null) zipStream.close();
} catch (IOException ex) {
Timber.w(ex);
}
}
// we need the keys at least
if (keys) return walletName;
else return null;
}
final static Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$");
private String getUniqueName(String name) {
if (!(new File(walletRoot, name + ".keys")).exists()) // <name> does not exist => it's ok to use
return name;
File[] wallets = walletRoot.listFiles(
(dir, filename) -> {
Matcher m = WALLET_PATTERN.matcher(filename);
if (m.find())
return m.group(1).equals(name);
else return false;
});
if (wallets.length == 0) return name + " (1)";
int maxIndex = 0;
for (File wallet : wallets) {
try {
final Matcher m = WALLET_PATTERN.matcher(wallet.getName());
m.find();
final int index = Integer.parseInt(m.group(2));
if (index > maxIndex) maxIndex = index;
} catch (NumberFormatException ex) {
// this cannot happen & we can ignore it if it does
}
}
return name + " (" + (maxIndex + 1) + ")";
}
}

View File

@@ -172,7 +172,8 @@ public class ExchangeEditText extends LinearLayout {
}
protected void setCurrencyAdapter(Spinner spinner, List<String> currencies) {
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
if (Helper.SHOW_EXCHANGERATES)
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, currencies);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(spinnerAdapter);

View File

@@ -155,7 +155,8 @@ public class ExchangeView extends LinearLayout {
void setCurrencyAdapter(Spinner spinner) {
List<String> currencies = new ArrayList<>();
currencies.add(Helper.BASE_CRYPTO);
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
if (Helper.SHOW_EXCHANGERATES)
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, currencies);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(spinnerAdapter);
@@ -167,11 +168,7 @@ public class ExchangeView extends LinearLayout {
etAmount = findViewById(R.id.etAmount);
tvAmountB = findViewById(R.id.tvAmountB);
sCurrencyA = findViewById(R.id.sCurrencyA);
ArrayAdapter adapter = ArrayAdapter.createFromResource(getContext(), R.array.currency, R.layout.item_spinner);
adapter.setDropDownViewResource(R.layout.item_spinner_dropdown_item);
sCurrencyA.setAdapter(adapter);
sCurrencyB = findViewById(R.id.sCurrencyB);
sCurrencyB.setAdapter(adapter);
evExchange = findViewById(R.id.evExchange);
pbExchange = findViewById(R.id.pbExchange);

View File

@@ -60,6 +60,7 @@
</RelativeLayout>
<LinearLayout
android:id="@+id/llExchange"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

View File

@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="8dp">
<LinearLayout
android:layout_width="fill_parent"
android:orientation="vertical"
android:padding="8dp"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<TextView
android:id="@+id/tvName"
@@ -19,14 +19,31 @@
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="My First Subaddress" />
<TextView
android:id="@+id/tvAddress"
style="@style/MoneroText.Small"
android:layout_width="match_parent"
android:layout_marginTop="4dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName"
tools:text="#1: 8AioXCmK...aGivEa7C" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/tx_amount"
style="@style/MoneroText.PosAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/tvAddress"
android:textColor="@color/tx_plus"
app:layout_constraintBottom_toBottomOf="@id/tvName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="+ 999.999999" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -18,19 +18,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="16dp"
android:layout_toStartOf="@+id/ibOptions"
tools:text="My Wallet" />
<TextView
android:id="@+id/tvAddress"
style="@style/MoneroText.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/tvName"
android:layout_marginStart="48dp"
android:layout_marginBottom="16dp"
android:layout_toStartOf="@+id/ibOptions"
tools:text="4AdkPJox...g3viqCuk" />
tools:text="My Wallet" />
<ImageButton
android:id="@+id/ibOptions"

View File

@@ -19,7 +19,7 @@
<item
android:id="@+id/action_archive"
android:orderInCategory="400"
android:title="@string/menu_archive" />
android:title="@string/menu_delete" />
<item
android:id="@+id/action_info"

View File

@@ -33,4 +33,9 @@
android:title="@string/menu_daynight"
app:showAsAction="never" />
<item
android:id="@+id/action_restore"
android:orderInCategory="600"
android:title="@string/menu_restore"
app:showAsAction="never" />
</menu>

View File

@@ -9,7 +9,6 @@
<string name="menu_help">Ajuda</string>
<string name="menu_receive">Rebre</string>
<string name="menu_rename">Canvi de nom</string>
<string name="menu_archive">Arxivar</string>
<string name="menu_backup">Còpia de seguretat</string>
<string name="menu_changepw">Canvi de contrasenya</string>
@@ -94,7 +93,6 @@
<string name="backup_success">Còpia de seguretat amb èxit</string>
<string name="backup_failed">Còpia de seguretat fallida!</string>
<string name="archive_failed">Arxiu fallit!</string>
<string name="rename_failed">Canvi de nom fallit!</string>
<string name="changepw_failed">Canvi de contrasenya fallit!</string>
<string name="changepw_success">La contrasenya ha canviat</string>
@@ -273,9 +271,8 @@
<string name="details_alert_no">Torna\'m enrere!</string>
<string name="details_title">Detalls</string>
<string name="archive_alert_message">Es farà una còpia de seguretat del portamonedes i s\'esborrarà.</string>
<string name="archive_alert_yes">Si, fes-ho!</string>
<string name="archive_alert_no">No gràcies!</string>
<string name="delete_alert_yes">Si, fes-ho!</string>
<string name="delete_alert_no">No gràcies!</string>
<string name="fab_create_new">Crear un nou portamonedes</string>
<string name="fab_restore_viewonly">Restablir portamonedes de només lectura</string>
@@ -420,4 +417,10 @@
<string name="subaddress_select_label">Select a subaddress</string>
<string name="subaddress_details_hint">Long-press for details</string>
<string name="delete_alert_message">The wallet will be deleted!</string>
<string name="menu_delete">Delete</string><!-- like: "Delete wallet!" -->
<string name="delete_failed">Delete failed!</string>
<string name="menu_restore">Import wallet</string>
<string name="restore_failed">Import failed!</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More