mirror of
https://github.com/m2049r/xmrwallet
synced 2025-09-03 08:23:04 +02:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b576a9de3d | ||
![]() |
148faa00e4 | ||
![]() |
e82b471c14 | ||
![]() |
9ed92e5117 | ||
![]() |
303b3aa354 | ||
![]() |
d801a50962 | ||
![]() |
9cd8a75dc6 | ||
![]() |
002dfd5d58 | ||
![]() |
54e54b2a8a | ||
![]() |
aa768596a4 | ||
![]() |
c4958f6c54 | ||
![]() |
2c2a5314d4 | ||
![]() |
669516c60b | ||
![]() |
a56a29a6c4 | ||
![]() |
4e31f47482 | ||
![]() |
c1f14f9653 | ||
![]() |
2746c52d7b | ||
![]() |
5df323bacb | ||
![]() |
776cc26377 | ||
![]() |
bdfb6a90b6 | ||
![]() |
6db44dfab1 | ||
![]() |
c68ac7db6d | ||
![]() |
e09862e940 | ||
![]() |
c7bd7469a1 | ||
![]() |
b39857fd2e | ||
![]() |
8170f823ab | ||
![]() |
38c0ead45c | ||
![]() |
1d027c1694 | ||
![]() |
45dc21fbf7 | ||
![]() |
d4f4de234a | ||
![]() |
394d5471e3 | ||
![]() |
beb098adb3 | ||
![]() |
fda370d35b | ||
![]() |
99681e1bbb | ||
![]() |
21f44380b1 | ||
![]() |
f4c1af1bb8 | ||
![]() |
c002b81ebd | ||
![]() |
d2f07ba3b6 | ||
![]() |
24fc27b09e |
@@ -171,6 +171,10 @@ add_library(wallet-crypto STATIC IMPORTED)
|
||||
set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libwallet-crypto.a)
|
||||
|
||||
add_library(cryptonote_format_utils_basic STATIC IMPORTED)
|
||||
set_target_properties(cryptonote_format_utils_basic PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/monero/libcryptonote_format_utils_basic.a)
|
||||
|
||||
#############
|
||||
# System
|
||||
#############
|
||||
@@ -193,6 +197,7 @@ target_link_libraries( monerujo
|
||||
wallet
|
||||
cryptonote_core
|
||||
cryptonote_basic
|
||||
cryptonote_format_utils_basic
|
||||
mnemonics
|
||||
ringct
|
||||
ringct_basic
|
||||
|
@@ -1,14 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.3'
|
||||
ndkVersion '17.2.4988734'
|
||||
defaultConfig {
|
||||
applicationId "com.m2049r.xmrwallet"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 800
|
||||
versionName "1.18.0 'ChAdOx1'"
|
||||
targetSdkVersion 30
|
||||
versionCode 1100
|
||||
versionName "2.1.0 'Vertant'"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
@@ -123,14 +124,14 @@ 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'
|
||||
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation "com.squareup.okhttp3:okhttp:4.9.0"
|
||||
implementation "com.burgstaller:okhttp-digest:2.1"
|
||||
implementation "io.github.rburgst:okhttp-digest:2.5"
|
||||
implementation "com.jakewharton.timber:timber:4.7.1"
|
||||
|
||||
implementation 'com.nulab-inc:zxcvbn:1.3.0'
|
||||
|
@@ -4,20 +4,19 @@
|
||||
|
||||
<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.USE_FINGERPRINT" />
|
||||
<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:name=".XmrWalletApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/MyMaterialTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
@@ -36,7 +35,7 @@
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
android:label="@string/wallet_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="behind"/>
|
||||
android:screenOrientation="behind" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
|
@@ -914,7 +914,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_refreshAsync(JNIEnv *env, jobject instanc
|
||||
|
||||
//virtual void rescanBlockchainAsync() = 0;
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_rescanBlockchainAsync(JNIEnv *env, jobject instance) {
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_rescanBlockchainAsyncJ(JNIEnv *env, jobject instance) {
|
||||
Bitmonero::Wallet *wallet = getHandle<Bitmonero::Wallet>(env, instance);
|
||||
wallet->rescanBlockchainAsync();
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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,11 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||
|
||||
void onWalletBackup(String name);
|
||||
|
||||
void onWalletArchive(String walletName);
|
||||
void onWalletRestore();
|
||||
|
||||
void onWalletDelete(String walletName);
|
||||
|
||||
void onWalletDeleteCache(String walletName);
|
||||
|
||||
void onAddWallet(String type);
|
||||
|
||||
@@ -110,7 +112,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 +202,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 +211,34 @@ 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 if (id == R.id.action_deletecache) {
|
||||
activityCallback.onWalletDeleteCache(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 +257,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 +275,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 +345,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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) {
|
||||
|
@@ -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,10 +576,8 @@ 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
|
||||
if (!synced) { // first sync
|
||||
onProgress(-1);
|
||||
@@ -583,7 +586,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 +599,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 +648,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 +661,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 +684,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 +695,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 +706,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 +723,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 +864,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 +974,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();
|
||||
|
@@ -58,7 +58,6 @@ import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
@@ -126,8 +125,9 @@ 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)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(Objects.requireNonNull(getContext()), R.layout.item_spinner_balance, currencies);
|
||||
if (Helper.SHOW_EXCHANGERATES)
|
||||
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(requireContext(), R.layout.item_spinner_balance, currencies);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
sCurrency.setAdapter(spinnerAdapter);
|
||||
|
||||
@@ -345,13 +345,18 @@ public class WalletFragment extends Fragment
|
||||
// if account index has changed scroll to top?
|
||||
private int accountIndex = 0;
|
||||
|
||||
public void onRefreshed(final Wallet wallet, final boolean full) {
|
||||
public void onRefreshed(final Wallet wallet, boolean full) {
|
||||
Timber.d("onRefreshed(%b)", full);
|
||||
|
||||
if (adapter.needsTransactionUpdateOnNewBlock()) {
|
||||
wallet.getHistory().refresh();
|
||||
full = true;
|
||||
}
|
||||
if (full) {
|
||||
List<TransactionInfo> list = new ArrayList<>();
|
||||
final long streetHeight = activityCallback.getStreetModeHeight();
|
||||
Timber.d("StreetHeight=%d", streetHeight);
|
||||
wallet.getHistory().refresh();
|
||||
for (TransactionInfo info : wallet.getHistory().getAll()) {
|
||||
Timber.d("TxHeight=%d, Label=%s", info.blockheight, info.subaddressLabel);
|
||||
if ((info.isPending || (info.blockheight >= streetHeight))
|
||||
@@ -363,7 +368,6 @@ public class WalletFragment extends Fragment
|
||||
accountIndex = wallet.getAccountIndex();
|
||||
txlist.scrollToPosition(0);
|
||||
}
|
||||
|
||||
}
|
||||
updateStatus(wallet);
|
||||
}
|
||||
@@ -561,7 +565,7 @@ public class WalletFragment extends Fragment
|
||||
//TODO figure out why gunther disappears on return from send although he is still set
|
||||
if (enable) {
|
||||
if (streetGunther == null)
|
||||
streetGunther = ContextCompat.getDrawable(Objects.requireNonNull(getContext()), R.drawable.ic_gunther_streetmode);
|
||||
streetGunther = ContextCompat.getDrawable(requireContext(), R.drawable.ic_gunther_streetmode);
|
||||
ivStreetGunther.setImageDrawable(streetGunther);
|
||||
} else
|
||||
ivStreetGunther.setImageDrawable(null);
|
||||
|
@@ -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 <
|
||||
|
@@ -21,6 +21,7 @@ import android.os.Parcelable;
|
||||
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
// https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents
|
||||
public class TxData implements Parcelable {
|
||||
@@ -54,7 +55,7 @@ public class TxData implements Parcelable {
|
||||
}
|
||||
|
||||
public double getAmountAsDouble() {
|
||||
return 1.0 * amount / 1000000000000L;
|
||||
return 1.0 * amount / Helper.ONE_XMR;
|
||||
}
|
||||
|
||||
public int getMixin() {
|
||||
|
@@ -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);
|
||||
@@ -345,7 +349,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
|
||||
private boolean checkAddress() {
|
||||
boolean ok = checkAddressNoError();
|
||||
if (!ok) {
|
||||
if (possibleCryptos.isEmpty()) {
|
||||
etAddress.setError(getString(R.string.send_address_invalid));
|
||||
} else {
|
||||
etAddress.setError(null);
|
||||
@@ -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));
|
||||
|
@@ -139,7 +139,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.showKeyboard(getActivity());
|
||||
final long funds = getTotalFunds();
|
||||
maxFunds = 1.0 * funds / 1000000000000L;
|
||||
maxFunds = 1.0 * funds / Helper.ONE_XMR;
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
tvFunds.setText(getString(R.string.send_available,
|
||||
Wallet.getDisplayAmount(funds)));
|
||||
|
@@ -34,6 +34,7 @@ import com.m2049r.xmrwallet.service.shift.ShiftException;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.OkHttpHelper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
|
||||
@@ -89,7 +90,6 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
Timber.i(maxBtc + "/" + minBtc);
|
||||
@@ -179,7 +179,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
tvXmrToParms.setText(xmrParmText);
|
||||
|
||||
final long funds = getTotalFunds();
|
||||
double availableXmr = 1.0 * funds / 1000000000000L;
|
||||
double availableXmr = 1.0 * funds / Helper.ONE_XMR;
|
||||
|
||||
String availBtcString;
|
||||
String availXmrString;
|
||||
|
@@ -359,40 +359,22 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
}
|
||||
|
||||
private RequestQuote xmrtoQuote = null;
|
||||
private int stageARetries = 0;
|
||||
private final int RETRIES = 3;
|
||||
private double stageAPrice = 0;
|
||||
|
||||
private void processStageA(final RequestQuote requestQuote) {
|
||||
Timber.d("processCreateOrder %s", requestQuote.getId());
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
// verify the BTC amount is correct (price can change and we can only specify XMR amount)
|
||||
// verify the BTC amount is correct
|
||||
if (requestQuote.getBtcAmount() != txDataBtc.getBtcAmount()) {
|
||||
if (--stageARetries <= 0) {
|
||||
Timber.d("Failed to get quote");
|
||||
getView().post(() ->
|
||||
showStageError(ShiftError.Error.SERVICE.toString(),
|
||||
getString(R.string.shift_noquote),
|
||||
getString(R.string.shift_checkamount)));
|
||||
return; // just stop for now
|
||||
}
|
||||
if (stageAPrice == requestQuote.getPrice()) {
|
||||
// same price but different BTC amount - something else is wrong (e.g. too many decimals)
|
||||
Timber.d("Price unchanged");
|
||||
getView().post(() ->
|
||||
showStageError(ShiftError.Error.SERVICE.toString(),
|
||||
getString(R.string.shift_noquote),
|
||||
getString(R.string.shift_checkamount)));
|
||||
return; // just stop for now
|
||||
}
|
||||
stageAPrice = requestQuote.getPrice();
|
||||
// recalc XMR and try again
|
||||
txDataBtc.setAmount(txDataBtc.getBtcAmount() / requestQuote.getPrice());
|
||||
getView().post(this::stageAOneShot);
|
||||
return; // stageA will run in the main thread
|
||||
Timber.d("Failed to get quote");
|
||||
getView().post(() -> showStageError(ShiftError.Error.SERVICE.toString(),
|
||||
getString(R.string.shift_noquote),
|
||||
getString(R.string.shift_checkamount)));
|
||||
return; // just stop for now
|
||||
}
|
||||
xmrtoQuote = requestQuote;
|
||||
txDataBtc.setAmount(xmrtoQuote.getXmrAmount());
|
||||
getView().post(() -> {
|
||||
// show data from the actual quote as that is what is used to
|
||||
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
||||
df.setMaximumFractionDigits(12);
|
||||
final String btcAmount = df.format(xmrtoQuote.getBtcAmount());
|
||||
@@ -438,18 +420,12 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
}
|
||||
|
||||
private void stageA() {
|
||||
stageARetries = RETRIES;
|
||||
stageAOneShot();
|
||||
}
|
||||
|
||||
private void stageAOneShot() {
|
||||
if (!isResumed) return;
|
||||
Timber.d("Request Quote");
|
||||
xmrtoQuote = null;
|
||||
xmrtoOrder = null;
|
||||
showProgress(1, getString(R.string.label_send_progress_xmrto_create));
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
stageAPrice = 0;
|
||||
|
||||
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
|
||||
@Override
|
||||
@@ -473,7 +449,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
}
|
||||
};
|
||||
|
||||
getXmrToApi().requestQuote(txDataBtc.getAmountAsDouble(), callback);
|
||||
getXmrToApi().requestQuote(txDataBtc.getBtcAmount(), callback);
|
||||
}
|
||||
|
||||
private CreateOrder xmrtoOrder = null;
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -30,6 +30,7 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.data.UserNotes;
|
||||
@@ -48,7 +49,7 @@ import java.util.TimeZone;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
|
||||
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
private final static SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
private final int outboundColour;
|
||||
private final int inboundColour;
|
||||
@@ -77,6 +78,10 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
DATETIME_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
public boolean needsTransactionUpdateOnNewBlock() {
|
||||
return (infoItems.size() > 0) && !infoItems.get(0).isConfirmed();
|
||||
}
|
||||
|
||||
private static class TransactionInfoDiff extends DiffCallback<TransactionInfo> {
|
||||
|
||||
public TransactionInfoDiff(List<TransactionInfo> oldList, List<TransactionInfo> newList) {
|
||||
@@ -95,6 +100,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
return (oldItem.direction == newItem.direction)
|
||||
&& (oldItem.isPending == newItem.isPending)
|
||||
&& (oldItem.isFailed == newItem.isFailed)
|
||||
&& (oldItem.confirmations == newItem.confirmations)
|
||||
&& (oldItem.subaddressLabel.equals(newItem.subaddressLabel))
|
||||
&& (Objects.equals(oldItem.notes, newItem.notes));
|
||||
}
|
||||
@@ -149,6 +155,8 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
final TextView tvFee;
|
||||
final TextView tvPaymentId;
|
||||
final TextView tvDateTime;
|
||||
final CircularProgressIndicator pbConfirmations;
|
||||
final TextView tvConfirmations;
|
||||
TransactionInfo infoItem;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
@@ -158,6 +166,9 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
tvFee = itemView.findViewById(R.id.tx_fee);
|
||||
tvPaymentId = itemView.findViewById(R.id.tx_paymentid);
|
||||
tvDateTime = itemView.findViewById(R.id.tx_datetime);
|
||||
pbConfirmations = itemView.findViewById(R.id.pbConfirmations);
|
||||
pbConfirmations.setMax(TransactionInfo.CONFIRMATION);
|
||||
tvConfirmations = itemView.findViewById(R.id.tvConfirmations);
|
||||
}
|
||||
|
||||
private String getDateTime(long time) {
|
||||
@@ -182,7 +193,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
ivTxType.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
ivTxType.setVisibility(View.GONE); // gives us more space for the amount
|
||||
ivTxType.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String displayAmount = Helper.getDisplayAmount(infoItem.amount, Helper.DISPLAY_DIGITS_INFO);
|
||||
@@ -205,12 +216,34 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
this.tvFee.setText(context.getString(R.string.tx_list_failed_text));
|
||||
tvFee.setVisibility(View.VISIBLE);
|
||||
setTxColour(failedColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else if (infoItem.isPending) {
|
||||
setTxColour(pendingColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
pbConfirmations.setIndeterminate(true);
|
||||
pbConfirmations.setVisibility(View.VISIBLE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else if (infoItem.direction == TransactionInfo.Direction.Direction_In) {
|
||||
setTxColour(inboundColour);
|
||||
if (!infoItem.isConfirmed()) {
|
||||
pbConfirmations.setVisibility(View.VISIBLE);
|
||||
final int confirmations = (int) infoItem.confirmations;
|
||||
pbConfirmations.setProgressCompat(confirmations, true);
|
||||
final String confCount = Integer.toString(confirmations);
|
||||
tvConfirmations.setText(confCount);
|
||||
if (confCount.length() == 1) // we only have space for character in the progress circle
|
||||
tvConfirmations.setVisibility(View.VISIBLE);
|
||||
else
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else {
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
setTxColour(outboundColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String tag = null;
|
||||
|
@@ -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
|
||||
|
@@ -29,6 +29,8 @@ 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<TransactionInfo> {
|
||||
public static final int CONFIRMATION = 10; // blocks
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum Direction {
|
||||
Direction_In(0),
|
||||
@@ -98,6 +100,10 @@ public class TransactionInfo implements Parcelable, Comparable<TransactionInfo>
|
||||
this.transfers = transfers;
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmations >= CONFIRMATION;
|
||||
}
|
||||
|
||||
public String getDisplayLabel() {
|
||||
if (subaddressLabel.isEmpty() || (Subaddress.DEFAULT_LABEL_FORMATTER.matcher(subaddressLabel).matches()))
|
||||
return ("#" + addressIndex);
|
||||
|
@@ -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();
|
||||
@@ -264,12 +273,14 @@ public class Wallet {
|
||||
|
||||
public native long getDaemonBlockChainTargetHeight();
|
||||
|
||||
public native boolean isSynchronizedJ();
|
||||
boolean synced = false;
|
||||
|
||||
public boolean isSynchronized() {
|
||||
final long daemonHeight = getDaemonBlockChainHeight();
|
||||
if (daemonHeight == 0) return false;
|
||||
return isSynchronizedJ() && (getBlockChainHeight() == daemonHeight);
|
||||
return synced;
|
||||
}
|
||||
|
||||
public void setSynchronized() {
|
||||
this.synced = true;
|
||||
}
|
||||
|
||||
public static native String getDisplayAmount(long amount);
|
||||
@@ -300,7 +311,12 @@ public class Wallet {
|
||||
|
||||
public native void refreshAsync();
|
||||
|
||||
public native void rescanBlockchainAsync();
|
||||
public native void rescanBlockchainAsyncJ();
|
||||
|
||||
public void rescanBlockchainAsync() {
|
||||
synced = false;
|
||||
rescanBlockchainAsyncJ();
|
||||
}
|
||||
|
||||
//TODO virtual void setAutoRefreshInterval(int millis) = 0;
|
||||
//TODO virtual int autoRefreshInterval() const = 0;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -108,16 +108,16 @@ public class WalletService extends Service {
|
||||
Timber.d("unconfirmedMoneyReceived() %d @ %s", amount, txId);
|
||||
}
|
||||
|
||||
long lastBlockTime = 0;
|
||||
int lastTxCount = 0;
|
||||
private long lastBlockTime = 0;
|
||||
private int lastTxCount = 0;
|
||||
|
||||
public void newBlock(long height) {
|
||||
Wallet wallet = getWallet();
|
||||
final Wallet wallet = getWallet();
|
||||
if (wallet == null) throw new IllegalStateException("No wallet!");
|
||||
// don't flood with an update for every block ...
|
||||
if (lastBlockTime < System.currentTimeMillis() - 2000) {
|
||||
Timber.d("newBlock() @ %d with observer %s", height, observer);
|
||||
lastBlockTime = System.currentTimeMillis();
|
||||
Timber.d("newBlock() @ %d with observer %s", height, observer);
|
||||
if (observer != null) {
|
||||
boolean fullRefresh = false;
|
||||
updateDaemonState(wallet, wallet.isSynchronized() ? height : 0);
|
||||
@@ -145,17 +145,16 @@ public class WalletService extends Service {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
public void refreshed() {
|
||||
public void refreshed() { // this means it's synced
|
||||
Timber.d("refreshed()");
|
||||
Wallet wallet = getWallet();
|
||||
final Wallet wallet = getWallet();
|
||||
if (wallet == null) throw new IllegalStateException("No wallet!");
|
||||
wallet.setSynchronized();
|
||||
if (updated) {
|
||||
updateDaemonState(wallet, wallet.getBlockChainHeight());
|
||||
wallet.getHistory().refreshWithNotes(wallet);
|
||||
if (observer != null) {
|
||||
updateDaemonState(wallet, 0);
|
||||
wallet.getHistory().refreshWithNotes(wallet);
|
||||
if (observer != null) {
|
||||
updated = !observer.onRefreshed(wallet, true);
|
||||
}
|
||||
updated = !observer.onRefreshed(wallet, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -167,7 +167,7 @@ public class ExchangeApiImpl implements ExchangeApi {
|
||||
private Calendar fetchDate = null;
|
||||
|
||||
synchronized private ExchangeRate getRate(String currency) throws ExchangeException {
|
||||
Timber.e("Getting %s", currency);
|
||||
Timber.d("Getting %s", currency);
|
||||
final Double rate = fxEntries.get(currency);
|
||||
if (rate == null) throw new ExchangeException(404, "Currency not supported: " + currency);
|
||||
return new ExchangeRateImpl(currency, rate, fxDate.getTime());
|
||||
|
@@ -74,10 +74,10 @@ class RequestQuoteImpl implements RequestQuote {
|
||||
price = jsonObject.getDouble("rate");
|
||||
}
|
||||
|
||||
public static void call(@NonNull final ShiftApiCall api, final double xmrAmount,
|
||||
public static void call(@NonNull final ShiftApiCall api, final double btcAmount,
|
||||
@NonNull final ShiftCallback<RequestQuote> callback) {
|
||||
try {
|
||||
final JSONObject request = createRequest(xmrAmount);
|
||||
final JSONObject request = createRequest(btcAmount);
|
||||
api.call("quotes", request, new NetworkCallback() {
|
||||
@Override
|
||||
public void onSuccess(JSONObject jsonObject) {
|
||||
@@ -104,13 +104,13 @@ class RequestQuoteImpl implements RequestQuote {
|
||||
* @param xmrAmount how much XMR to shift to BTC
|
||||
*/
|
||||
|
||||
static JSONObject createRequest(final double xmrAmount) throws JSONException {
|
||||
static JSONObject createRequest(final double btcAmount) throws JSONException {
|
||||
final JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("depositMethod", "xmr");
|
||||
jsonObject.put("settleMethod", ServiceHelper.ASSET);
|
||||
// #sideshift is silly and likes numbers as strings
|
||||
String amount = AmountFormatter.format(xmrAmount);
|
||||
jsonObject.put("depositAmount", amount);
|
||||
String amount = AmountFormatter.format(btcAmount);
|
||||
jsonObject.put("settleAmount", amount);
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
|
@@ -64,8 +64,8 @@ public class SideShiftApiImpl implements SideShiftApi, ShiftApiCall {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestQuote(final double xmrAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
|
||||
RequestQuoteImpl.call(this, xmrAmount, callback);
|
||||
public void requestQuote(final double btcAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
|
||||
RequestQuoteImpl.call(this, btcAmount, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -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,17 @@ 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 public final long ONE_XMR = Math.round(Math.pow(10, Helper.XMR_DECIMALS));
|
||||
|
||||
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 +96,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 +109,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 +134,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 +161,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 +197,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 +311,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 +327,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);
|
||||
|
@@ -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.
|
||||
|
@@ -0,0 +1,170 @@
|
||||
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)) {
|
||||
// can't migrate - don't remember this, as the user may turn on permissions later
|
||||
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();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user