You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1224 lines
43 KiB
1224 lines
43 KiB
/*
|
|
* Copyright (c) 2017 m2049r
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.m2049r.xmrwallet;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.content.ServiceConnection;
|
|
import android.content.SharedPreferences;
|
|
import android.content.pm.PackageManager;
|
|
import android.os.AsyncTask;
|
|
import android.os.Bundle;
|
|
import android.os.IBinder;
|
|
import android.view.KeyEvent;
|
|
import android.view.LayoutInflater;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.widget.EditText;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.appcompat.app.ActionBarDrawerToggle;
|
|
import androidx.appcompat.app.AlertDialog;
|
|
import androidx.core.view.GravityCompat;
|
|
import androidx.drawerlayout.widget.DrawerLayout;
|
|
import androidx.fragment.app.Fragment;
|
|
import androidx.fragment.app.FragmentManager;
|
|
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|
import com.google.android.material.navigation.NavigationView;
|
|
import com.m2049r.xmrwallet.data.BarcodeData;
|
|
import com.m2049r.xmrwallet.data.Subaddress;
|
|
import com.m2049r.xmrwallet.data.TxData;
|
|
import com.m2049r.xmrwallet.data.UserNotes;
|
|
import com.m2049r.xmrwallet.dialog.CreditsFragment;
|
|
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
|
import com.m2049r.xmrwallet.fragment.send.SendAddressWizardFragment;
|
|
import com.m2049r.xmrwallet.fragment.send.SendFragment;
|
|
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
|
|
import com.m2049r.xmrwallet.model.PendingTransaction;
|
|
import com.m2049r.xmrwallet.model.TransactionInfo;
|
|
import com.m2049r.xmrwallet.model.Wallet;
|
|
import com.m2049r.xmrwallet.model.WalletManager;
|
|
import com.m2049r.xmrwallet.service.WalletService;
|
|
import com.m2049r.xmrwallet.util.Helper;
|
|
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
|
|
import com.m2049r.xmrwallet.util.ThemeHelper;
|
|
import com.m2049r.xmrwallet.widget.Toolbar;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import timber.log.Timber;
|
|
|
|
public class WalletActivity extends BaseActivity implements WalletFragment.Listener,
|
|
WalletService.Observer, SendFragment.Listener, TxFragment.Listener,
|
|
GenerateReviewFragment.ListenerWithWallet,
|
|
GenerateReviewFragment.Listener,
|
|
GenerateReviewFragment.PasswordChangedListener,
|
|
ScannerFragment.OnScannedListener, ReceiveFragment.Listener,
|
|
SendAddressWizardFragment.OnScanListener,
|
|
WalletFragment.DrawerLocker,
|
|
NavigationView.OnNavigationItemSelectedListener,
|
|
SubaddressFragment.Listener,
|
|
SubaddressInfoFragment.Listener {
|
|
|
|
public static final String REQUEST_ID = "id";
|
|
public static final String REQUEST_PW = "pw";
|
|
public static final String REQUEST_FINGERPRINT_USED = "fingerprint";
|
|
public static final String REQUEST_STREETMODE = "streetmode";
|
|
public static final String REQUEST_URI = "uri";
|
|
|
|
private NavigationView accountsView;
|
|
private DrawerLayout drawer;
|
|
private ActionBarDrawerToggle drawerToggle;
|
|
|
|
private Toolbar toolbar;
|
|
private boolean requestStreetMode = false;
|
|
|
|
private String password;
|
|
|
|
private String uri = null;
|
|
|
|
private long streetMode = 0;
|
|
|
|
@Override
|
|
public void onPasswordChanged(String newPassword) {
|
|
password = newPassword;
|
|
}
|
|
|
|
@Override
|
|
public String getPassword() {
|
|
return password;
|
|
}
|
|
|
|
@Override
|
|
public void setToolbarButton(int type) {
|
|
toolbar.setButton(type);
|
|
}
|
|
|
|
@Override
|
|
public void setTitle(String title, String subtitle) {
|
|
toolbar.setTitle(title, subtitle);
|
|
}
|
|
|
|
@Override
|
|
public void setTitle(String title) {
|
|
Timber.d("setTitle:%s.", title);
|
|
toolbar.setTitle(title);
|
|
}
|
|
|
|
@Override
|
|
public void setSubtitle(String subtitle) {
|
|
toolbar.setSubtitle(subtitle);
|
|
}
|
|
|
|
private boolean synced = false;
|
|
|
|
@Override
|
|
public boolean isSynced() {
|
|
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;
|
|
}
|
|
|
|
private void enableStreetMode(boolean enable) {
|
|
if (enable) {
|
|
streetMode = getWallet().getDaemonBlockChainHeight();
|
|
} else {
|
|
streetMode = 0;
|
|
}
|
|
final WalletFragment walletFragment = getWalletFragment();
|
|
if (walletFragment != null) walletFragment.resetDismissedTransactions();
|
|
forceUpdate();
|
|
runOnUiThread(() -> {
|
|
if (getWallet() != null)
|
|
updateAccountsBalance();
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public long getStreetModeHeight() {
|
|
return streetMode;
|
|
}
|
|
|
|
@Override
|
|
public boolean isWatchOnly() {
|
|
return getWallet().isWatchOnly();
|
|
}
|
|
|
|
@Override
|
|
public String getTxKey(String txId) {
|
|
return getWallet().getTxKey(txId);
|
|
}
|
|
|
|
@Override
|
|
public String getTxNotes(String txId) {
|
|
return getWallet().getUserNote(txId);
|
|
}
|
|
|
|
@Override
|
|
public boolean setTxNotes(String txId, String txNotes) {
|
|
return getWallet().setUserNote(txId, txNotes);
|
|
}
|
|
|
|
@Override
|
|
public String getTxAddress(int major, int minor) {
|
|
return getWallet().getSubaddress(major, minor);
|
|
}
|
|
|
|
@Override
|
|
protected void onStart() {
|
|
super.onStart();
|
|
Timber.d("onStart()");
|
|
}
|
|
|
|
private void startWalletService() {
|
|
Bundle extras = getIntent().getExtras();
|
|
if (extras != null) {
|
|
acquireWakeLock();
|
|
String walletId = extras.getString(REQUEST_ID);
|
|
// we can set the streetmode height AFTER opening the wallet
|
|
requestStreetMode = extras.getBoolean(REQUEST_STREETMODE);
|
|
password = extras.getString(REQUEST_PW);
|
|
uri = extras.getString(REQUEST_URI);
|
|
connectWalletService(walletId, password);
|
|
} else {
|
|
finish();
|
|
}
|
|
}
|
|
|
|
private void stopWalletService() {
|
|
disconnectWalletService();
|
|
releaseWakeLock();
|
|
}
|
|
|
|
private void onWalletRescan() {
|
|
try {
|
|
final WalletFragment walletFragment = getWalletFragment();
|
|
getWallet().rescanBlockchainAsync();
|
|
synced = false;
|
|
walletFragment.unsync();
|
|
invalidateOptionsMenu();
|
|
} catch (ClassCastException ex) {
|
|
Timber.d(ex.getLocalizedMessage());
|
|
// keep calm and carry on
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onStop() {
|
|
Timber.d("onStop()");
|
|
super.onStop();
|
|
}
|
|
|
|
@Override
|
|
protected void onDestroy() {
|
|
Timber.d("onDestroy()");
|
|
if ((mBoundService != null) && (getWallet() != null)) {
|
|
saveWallet();
|
|
}
|
|
stopWalletService();
|
|
if (drawer != null) drawer.removeDrawerListener(drawerToggle);
|
|
super.onDestroy();
|
|
}
|
|
|
|
@Override
|
|
public boolean hasWallet() {
|
|
return haveWallet;
|
|
}
|
|
|
|
@Override
|
|
public boolean onPrepareOptionsMenu(Menu menu) {
|
|
MenuItem renameItem = menu.findItem(R.id.action_rename);
|
|
if (renameItem != null)
|
|
renameItem.setEnabled(hasWallet() && getWallet().isSynchronized());
|
|
MenuItem streetmodeItem = menu.findItem(R.id.action_streetmode);
|
|
if (streetmodeItem != null)
|
|
if (isStreetMode()) {
|
|
streetmodeItem.setIcon(R.drawable.gunther_csi_24dp);
|
|
} else {
|
|
streetmodeItem.setIcon(R.drawable.gunther_24dp);
|
|
}
|
|
final MenuItem rescanItem = menu.findItem(R.id.action_rescan);
|
|
if (rescanItem != null)
|
|
rescanItem.setEnabled(isSynced());
|
|
return super.onPrepareOptionsMenu(menu);
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
final int itemId = item.getItemId();
|
|
if (itemId == R.id.action_rescan) {
|
|
onWalletRescan();
|
|
} else if (itemId == R.id.action_info) {
|
|
onWalletDetails();
|
|
} else if (itemId == R.id.action_credits) {
|
|
CreditsFragment.display(getSupportFragmentManager());
|
|
} else if (itemId == R.id.action_share) {
|
|
onShareTxInfo();
|
|
} else if (itemId == R.id.action_help_tx_info) {
|
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_tx_details);
|
|
} else if (itemId == R.id.action_help_wallet) {
|
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_wallet);
|
|
} else if (itemId == R.id.action_details_help) {
|
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_details);
|
|
} else if (itemId == R.id.action_details_changepw) {
|
|
onWalletChangePassword();
|
|
} else if (itemId == R.id.action_help_send) {
|
|
HelpFragment.display(getSupportFragmentManager(), R.string.help_send);
|
|
} else if (itemId == R.id.action_rename) {
|
|
onAccountRename();
|
|
} else if (itemId == R.id.action_subaddresses) {
|
|
showSubaddresses(true);
|
|
} else if (itemId == R.id.action_streetmode) {
|
|
if (isStreetMode()) { // disable streetmode
|
|
onDisableStreetMode();
|
|
} else {
|
|
onEnableStreetMode();
|
|
}
|
|
} else
|
|
return super.onOptionsItemSelected(item);
|
|
return true;
|
|
}
|
|
|
|
private void updateStreetMode() {
|
|
invalidateOptionsMenu();
|
|
}
|
|
|
|
private void onEnableStreetMode() {
|
|
enableStreetMode(true);
|
|
updateStreetMode();
|
|
}
|
|
|
|
private void onDisableStreetMode() {
|
|
Helper.promptPassword(WalletActivity.this, getWallet().getName(), false, new Helper.PasswordAction() {
|
|
@Override
|
|
public void act(String walletName, String password, boolean fingerprintUsed) {
|
|
runOnUiThread(() -> {
|
|
enableStreetMode(false);
|
|
updateStreetMode();
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void fail(String walletName) {
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
public void onWalletChangePassword() {
|
|
try {
|
|
GenerateReviewFragment detailsFragment = (GenerateReviewFragment) getCurrentFragment();
|
|
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
|
|
if (dialog != null) {
|
|
Helper.showKeyboard(dialog);
|
|
dialog.show();
|
|
}
|
|
} catch (ClassCastException ex) {
|
|
Timber.w("onWalletChangePassword() called, but no GenerateReviewFragment active");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
Timber.d("onCreate()");
|
|
super.onCreate(savedInstanceState);
|
|
if (savedInstanceState != null) {
|
|
// activity restarted
|
|
// we don't want that - finish it and fall back to previous activity
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
setContentView(R.layout.activity_wallet);
|
|
toolbar = findViewById(R.id.toolbar);
|
|
setSupportActionBar(toolbar);
|
|
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
|
|
|
toolbar.setOnButtonListener(new Toolbar.OnButtonListener() {
|
|
@Override
|
|
public void onButton(int type) {
|
|
switch (type) {
|
|
case Toolbar.BUTTON_BACK:
|
|
onDisposeRequest();
|
|
onBackPressed();
|
|
break;
|
|
case Toolbar.BUTTON_CANCEL:
|
|
onDisposeRequest();
|
|
Helper.hideKeyboard(WalletActivity.this);
|
|
WalletActivity.super.onBackPressed();
|
|
break;
|
|
case Toolbar.BUTTON_CLOSE:
|
|
finish();
|
|
break;
|
|
case Toolbar.BUTTON_CREDITS:
|
|
Toast.makeText(WalletActivity.this, getString(R.string.label_credits), Toast.LENGTH_SHORT).show();
|
|
case Toolbar.BUTTON_NONE:
|
|
default:
|
|
Timber.e("Button " + type + "pressed - how can this be?");
|
|
}
|
|
}
|
|
});
|
|
|
|
drawer = findViewById(R.id.drawer_layout);
|
|
drawerToggle = new ActionBarDrawerToggle(this, drawer, toolbar, 0, 0);
|
|
drawer.addDrawerListener(drawerToggle);
|
|
drawerToggle.syncState();
|
|
setDrawerEnabled(false); // disable until synced
|
|
|
|
accountsView = findViewById(R.id.accounts_nav);
|
|
accountsView.setNavigationItemSelectedListener(this);
|
|
|
|
showNet();
|
|
|
|
Fragment walletFragment = new WalletFragment();
|
|
getSupportFragmentManager().beginTransaction()
|
|
.add(R.id.fragment_container, walletFragment, WalletFragment.class.getName()).commit();
|
|
Timber.d("fragment added");
|
|
|
|
startWalletService();
|
|
Timber.d("onCreate() done.");
|
|
}
|
|
|
|
public void showNet() {
|
|
switch (WalletManager.getInstance().getNetworkType()) {
|
|
case NetworkType_Mainnet:
|
|
toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet);
|
|
break;
|
|
case NetworkType_Stagenet:
|
|
case NetworkType_Testnet:
|
|
toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark));
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Unsupported Network: " + WalletManager.getInstance().getNetworkType());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Wallet getWallet() {
|
|
if (mBoundService == null) throw new IllegalStateException("WalletService not bound.");
|
|
return mBoundService.getWallet();
|
|
}
|
|
|
|
private WalletService mBoundService = null;
|
|
private boolean mIsBound = false;
|
|
|
|
private final ServiceConnection mConnection = new ServiceConnection() {
|
|
public void onServiceConnected(ComponentName className, IBinder service) {
|
|
// This is called when the connection with the service has been
|
|
// established, giving us the service object we can use to
|
|
// interact with the service. Because we have bound to a explicit
|
|
// service that we know is running in our own process, we can
|
|
// cast its IBinder to a concrete class and directly access it.
|
|
mBoundService = ((WalletService.WalletServiceBinder) service).getService();
|
|
mBoundService.setObserver(WalletActivity.this);
|
|
Bundle extras = getIntent().getExtras();
|
|
if (extras != null) {
|
|
String walletId = extras.getString(REQUEST_ID);
|
|
if (walletId != null) {
|
|
setTitle(walletId, getString(R.string.status_wallet_connecting));
|
|
}
|
|
}
|
|
updateProgress();
|
|
Timber.d("CONNECTED");
|
|
}
|
|
|
|
public void onServiceDisconnected(ComponentName className) {
|
|
// This is called when the connection with the service has been
|
|
// unexpectedly disconnected -- that is, its process crashed.
|
|
// Because it is running in our same process, we should never
|
|
// see this happen.
|
|
mBoundService = null;
|
|
setTitle(getString(R.string.wallet_activity_name), getString(R.string.status_wallet_disconnected));
|
|
Timber.d("DISCONNECTED");
|
|
}
|
|
};
|
|
|
|
void connectWalletService(String walletName, String walletPassword) {
|
|
// Establish a connection with the service. We use an explicit
|
|
// class name because we want a specific service implementation that
|
|
// we know will be running in our own process (and thus won't be
|
|
// supporting component replacement by other applications).
|
|
Intent intent = new Intent(getApplicationContext(), WalletService.class);
|
|
intent.putExtra(WalletService.REQUEST_WALLET, walletName);
|
|
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_LOAD);
|
|
intent.putExtra(WalletService.REQUEST_CMD_LOAD_PW, walletPassword);
|
|
startService(intent);
|
|
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
|
|
mIsBound = true;
|
|
Timber.d("BOUND");
|
|
}
|
|
|
|
void disconnectWalletService() {
|
|
if (mIsBound) {
|
|
// Detach our existing connection.
|
|
mBoundService.setObserver(null);
|
|
unbindService(mConnection);
|
|
mIsBound = false;
|
|
Timber.d("UNBOUND");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onPause() {
|
|
Timber.d("onPause()");
|
|
super.onPause();
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
super.onResume();
|
|
Timber.d("onResume()");
|
|
}
|
|
|
|
public void saveWallet() {
|
|
if (mIsBound) { // no point in talking to unbound service
|
|
Intent intent = new Intent(getApplicationContext(), WalletService.class);
|
|
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_STORE);
|
|
startService(intent);
|
|
Timber.d("STORE request sent");
|
|
} else {
|
|
Timber.e("Service not bound");
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////
|
|
// WalletFragment.Listener
|
|
//////////////////////////////////////////
|
|
|
|
@Override
|
|
public boolean hasBoundService() {
|
|
return mBoundService != null;
|
|
}
|
|
|
|
@Override
|
|
public Wallet.ConnectionStatus getConnectionStatus() {
|
|
return mBoundService.getConnectionStatus();
|
|
}
|
|
|
|
@Override
|
|
public long getDaemonHeight() {
|
|
return mBoundService.getDaemonHeight();
|
|
}
|
|
|
|
@Override
|
|
public void onSendRequest(View view) {
|
|
replaceFragmentWithTransition(view, SendFragment.newInstance(uri), null, null);
|
|
uri = null; // only use uri once
|
|
}
|
|
|
|
@Override
|
|
public void onTxDetailsRequest(View view, TransactionInfo info) {
|
|
Bundle args = new Bundle();
|
|
args.putParcelable(TxFragment.ARG_INFO, info);
|
|
replaceFragmentWithTransition(view, new TxFragment(), null, args);
|
|
}
|
|
|
|
@Override
|
|
public void forceUpdate() {
|
|
try {
|
|
onRefreshed(getWallet(), true);
|
|
} catch (IllegalStateException ex) {
|
|
Timber.e(ex.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
///////////////////////////
|
|
// WalletService.Observer
|
|
///////////////////////////
|
|
|
|
private int numAccounts = -1;
|
|
|
|
// refresh and return true if successful
|
|
@Override
|
|
public boolean onRefreshed(final Wallet wallet, final boolean full) {
|
|
Timber.d("onRefreshed()");
|
|
runOnUiThread(() -> {
|
|
if (getWallet() != null)
|
|
updateAccountsBalance();
|
|
});
|
|
if (numAccounts != wallet.getNumAccounts()) {
|
|
numAccounts = wallet.getNumAccounts();
|
|
runOnUiThread(this::updateAccountsList);
|
|
}
|
|
try {
|
|
final WalletFragment walletFragment = getWalletFragment();
|
|
if (wallet.isSynchronized()) {
|
|
releaseWakeLock(RELEASE_WAKE_LOCK_DELAY); // the idea is to stay awake until synced
|
|
if (!synced) { // first sync
|
|
onProgress(-1);
|
|
saveWallet(); // save on first sync
|
|
synced = true;
|
|
runOnUiThread(walletFragment::onSynced);
|
|
}
|
|
}
|
|
runOnUiThread(() -> {
|
|
walletFragment.onRefreshed(wallet, full);
|
|
updateCurrentFragment(wallet);
|
|
});
|
|
return true;
|
|
} catch (ClassCastException ex) {
|
|
// not in wallet fragment (probably send monero)
|
|
Timber.d(ex.getLocalizedMessage());
|
|
// keep calm and carry on
|
|
}
|
|
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(() -> {
|
|
if (success) {
|
|
Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_unloaded), Toast.LENGTH_SHORT).show();
|
|
} else {
|
|
Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_unload_failed), Toast.LENGTH_LONG).show();
|
|
}
|
|
});
|
|
}
|
|
|
|
boolean haveWallet = false;
|
|
|
|
@Override
|
|
public void onWalletOpen(final Wallet.Device device) {
|
|
if (device == Wallet.Device.Device_Ledger) {
|
|
runOnUiThread(() -> showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onWalletStarted(final Wallet.Status walletStatus) {
|
|
runOnUiThread(() -> {
|
|
dismissProgressDialog();
|
|
if (walletStatus == null) {
|
|
// guess what went wrong
|
|
Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_connect_failed), Toast.LENGTH_LONG).show();
|
|
} else {
|
|
if (Wallet.ConnectionStatus.ConnectionStatus_WrongVersion == walletStatus.getConnectionStatus())
|
|
Toast.makeText(WalletActivity.this, getString(R.string.status_wallet_connect_wrongversion), Toast.LENGTH_LONG).show();
|
|
else if (!walletStatus.isOk())
|
|
Toast.makeText(WalletActivity.this, walletStatus.getErrorString(), Toast.LENGTH_LONG).show();
|
|
}
|
|
});
|
|
if ((walletStatus == null) || (Wallet.ConnectionStatus.ConnectionStatus_Connected != walletStatus.getConnectionStatus())) {
|
|
finish();
|
|
} else {
|
|
haveWallet = true;
|
|
invalidateOptionsMenu();
|
|
|
|
if (requestStreetMode) onEnableStreetMode();
|
|
|
|
final WalletFragment walletFragment = getWalletFragment();
|
|
runOnUiThread(() -> {
|
|
updateAccountsHeader();
|
|
if (walletFragment != null) {
|
|
walletFragment.onLoaded();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
|
try {
|
|
final SendFragment sendFragment = (SendFragment) getCurrentFragment();
|
|
runOnUiThread(() -> {
|
|
dismissProgressDialog();
|
|
PendingTransaction.Status status = pendingTransaction.getStatus();
|
|
if (status != PendingTransaction.Status.Status_Ok) {
|
|
String errorText = pendingTransaction.getErrorString();
|
|
getWallet().disposePendingTransaction();
|
|
sendFragment.onCreateTransactionFailed(errorText);
|
|
} else {
|
|
sendFragment.onTransactionCreated(txTag, pendingTransaction);
|
|
}
|
|
});
|
|
} catch (ClassCastException ex) {
|
|
// not in spend fragment
|
|
Timber.d(ex.getLocalizedMessage());
|
|
// don't need the transaction any more
|
|
getWallet().disposePendingTransaction();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onSendTransactionFailed(final String error) {
|
|
try {
|
|
final SendFragment sendFragment = (SendFragment) getCurrentFragment();
|
|
runOnUiThread(() -> sendFragment.onSendTransactionFailed(error));
|
|
} catch (ClassCastException ex) {
|
|
// not in spend fragment
|
|
Timber.d(ex.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTransactionSent(final String txId) {
|
|
try {
|
|
final SendFragment sendFragment = (SendFragment) getCurrentFragment();
|
|
runOnUiThread(() -> sendFragment.onTransactionSent(txId));
|
|
} catch (ClassCastException ex) {
|
|
// not in spend fragment
|
|
Timber.d(ex.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onProgress(final String text) {
|
|
try {
|
|
final WalletFragment walletFragment = getWalletFragment();
|
|
runOnUiThread(new Runnable() {
|
|
public void run() {
|
|
walletFragment.setProgress(text);
|
|
}
|
|
});
|
|
} catch (ClassCastException ex) {
|
|
// not in wallet fragment (probably send monero)
|
|
Timber.d(ex.getLocalizedMessage());
|
|
// keep calm and carry on
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onProgress(final int n) {
|
|
runOnUiThread(() -> {
|
|
try {
|
|
WalletFragment walletFragment = getWalletFragment();
|
|
if (walletFragment != null)
|
|
walletFragment.setProgress(n);
|
|
} catch (ClassCastException ex) {
|
|
// not in wallet fragment (probably send monero)
|
|
Timber.d(ex.getLocalizedMessage());
|
|
// keep calm and carry on
|
|
}
|
|
});
|
|
}
|
|
|
|
private void updateProgress() {
|
|
// TODO maybe show real state of WalletService (like "still closing previous wallet")
|
|
if (hasBoundService()) {
|
|
onProgress(mBoundService.getProgressText());
|
|
onProgress(mBoundService.getProgressValue());
|
|
}
|
|
}
|
|
|
|
///////////////////////////
|
|
// SendFragment.Listener
|
|
///////////////////////////
|
|
|
|
@Override
|
|
public void onSend(UserNotes notes) {
|
|
if (mIsBound) { // no point in talking to unbound service
|
|
Intent intent = new Intent(getApplicationContext(), WalletService.class);
|
|
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_SEND);
|
|
intent.putExtra(WalletService.REQUEST_CMD_SEND_NOTES, notes.txNotes);
|
|
startService(intent);
|
|
Timber.d("SEND TX request sent");
|
|
} else {
|
|
Timber.e("Service not bound");
|
|
}
|
|
|
|
}
|
|
|
|
@Override
|
|
public void onPrepareSend(final String tag, final TxData txData) {
|
|
if (mIsBound) { // no point in talking to unbound service
|
|
Intent intent = new Intent(getApplicationContext(), WalletService.class);
|
|
intent.putExtra(WalletService.REQUEST, WalletService.REQUEST_CMD_TX);
|
|
intent.putExtra(WalletService.REQUEST_CMD_TX_DATA, txData);
|
|
intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag);
|
|
startService(intent);
|
|
Timber.d("CREATE TX request sent");
|
|
if (getWallet().getDeviceType() == Wallet.Device.Device_Ledger)
|
|
showLedgerProgressDialog(LedgerProgressDialog.TYPE_SEND);
|
|
} else {
|
|
Timber.e("Service not bound");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Subaddress getWalletSubaddress(int accountIndex, int subaddressIndex) {
|
|
return getWallet().getSubaddressObject(accountIndex, subaddressIndex);
|
|
}
|
|
|
|
public String getWalletName() {
|
|
return getWallet().getName();
|
|
}
|
|
|
|
void popFragmentStack(String name) {
|
|
if (name == null) {
|
|
getSupportFragmentManager().popBackStack();
|
|
} else {
|
|
getSupportFragmentManager().popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
|
}
|
|
}
|
|
|
|
void replaceFragmentWithTransition(View view, Fragment newFragment, String stackName, Bundle extras) {
|
|
if (extras != null) {
|
|
newFragment.setArguments(extras);
|
|
}
|
|
int transition;
|
|
if (newFragment instanceof TxFragment)
|
|
transition = R.string.tx_details_transition_name;
|
|
else if (newFragment instanceof ReceiveFragment)
|
|
transition = R.string.receive_transition_name;
|
|
else if (newFragment instanceof SendFragment)
|
|
transition = R.string.send_transition_name;
|
|
else if (newFragment instanceof SubaddressInfoFragment)
|
|
transition = R.string.subaddress_info_transition_name;
|
|
else
|
|
throw new IllegalStateException("expecting known transition");
|
|
|
|
getSupportFragmentManager().beginTransaction()
|
|
.addSharedElement(view, getString(transition))
|
|
.replace(R.id.fragment_container, newFragment)
|
|
.addToBackStack(stackName)
|
|
.commit();
|
|
}
|
|
|
|
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
|
|
if (extras != null) {
|
|
newFragment.setArguments(extras);
|
|
}
|
|
getSupportFragmentManager()
|
|
.beginTransaction()
|
|
.replace(R.id.fragment_container, newFragment)
|
|
.addToBackStack(stackName)
|
|
.commit();
|
|
}
|
|
|
|
private void onWalletDetails() {
|
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
switch (which) {
|
|
case DialogInterface.BUTTON_POSITIVE:
|
|
final Bundle extras = new Bundle();
|
|
extras.putString(GenerateReviewFragment.REQUEST_TYPE, GenerateReviewFragment.VIEW_TYPE_WALLET);
|
|
|
|
Helper.promptPassword(WalletActivity.this, getWallet().getName(), true, new Helper.PasswordAction() {
|
|
@Override
|
|
public void act(String walletName, String password, boolean fingerprintUsed) {
|
|
replaceFragment(new GenerateReviewFragment(), null, extras);
|
|
}
|
|
|
|
@Override
|
|
public void fail(String walletName) {
|
|
}
|
|
});
|
|
|
|
break;
|
|
case DialogInterface.BUTTON_NEGATIVE:
|
|
// do nothing
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
|
builder.setMessage(getString(R.string.details_alert_message))
|
|
.setPositiveButton(getString(R.string.details_alert_yes), dialogClickListener)
|
|
.setNegativeButton(getString(R.string.details_alert_no), dialogClickListener)
|
|
.show();
|
|
}
|
|
|
|
void onShareTxInfo() {
|
|
try {
|
|
TxFragment fragment = (TxFragment) getCurrentFragment();
|
|
fragment.shareTxInfo();
|
|
} catch (ClassCastException ex) {
|
|
// not in wallet fragment
|
|
Timber.e(ex.getLocalizedMessage());
|
|
// keep calm and carry on
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDisposeRequest() {
|
|
//TODO consider doing this through the WalletService to avoid concurrency issues
|
|
getWallet().disposePendingTransaction();
|
|
}
|
|
|
|
private boolean startScanFragment = false;
|
|
|
|
@Override
|
|
protected void onResumeFragments() {
|
|
super.onResumeFragments();
|
|
if (startScanFragment) {
|
|
startScanFragment();
|
|
startScanFragment = false;
|
|
}
|
|
}
|
|
|
|
private void startScanFragment() {
|
|
Bundle extras = new Bundle();
|
|
replaceFragment(new ScannerFragment(), null, extras);
|
|
}
|
|
|
|
/// QR scanner callbacks
|
|
@Override
|
|
public void onScan() {
|
|
if (Helper.getCameraPermission(this)) {
|
|
startScanFragment();
|
|
} else {
|
|
Timber.i("Waiting for permissions");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onScanned(String qrCode) {
|
|
// #gurke
|
|
BarcodeData bcData = BarcodeData.fromString(qrCode);
|
|
if (bcData != null) {
|
|
popFragmentStack(null);
|
|
Timber.d("AAA");
|
|
onUriScanned(bcData);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
OnUriScannedListener onUriScannedListener = null;
|
|
|
|
@Override
|
|
public void setOnUriScannedListener(OnUriScannedListener onUriScannedListener) {
|
|
this.onUriScannedListener = onUriScannedListener;
|
|
}
|
|
|
|
@Override
|
|
void onUriScanned(BarcodeData barcodeData) {
|
|
super.onUriScanned(barcodeData);
|
|
boolean processed = false;
|
|
if (onUriScannedListener != null) {
|
|
processed = onUriScannedListener.onUriScanned(barcodeData);
|
|
}
|
|
if (!processed || (onUriScannedListener == null)) {
|
|
Toast.makeText(this, getString(R.string.nfc_tag_read_what), Toast.LENGTH_LONG).show();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
Timber.d("onRequestPermissionsResult()");
|
|
if (requestCode == Helper.PERMISSIONS_REQUEST_CAMERA) { // If request is cancelled, the result arrays are empty.
|
|
if (grantResults.length > 0
|
|
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
startScanFragment = true;
|
|
} else {
|
|
String msg = getString(R.string.message_camera_not_permitted);
|
|
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onWalletReceive(View view) {
|
|
final String address = getWallet().getAddress();
|
|
Timber.d("startReceive()");
|
|
Bundle b = new Bundle();
|
|
b.putString("address", address);
|
|
b.putString("name", getWalletName());
|
|
replaceFragmentWithTransition(view, new ReceiveFragment(), null, b);
|
|
Timber.d("ReceiveFragment placed");
|
|
}
|
|
|
|
@Override
|
|
public long getTotalFunds() {
|
|
return getWallet().getUnlockedBalance();
|
|
}
|
|
|
|
@Override
|
|
public void onBackPressed() {
|
|
if (drawer.isDrawerOpen(GravityCompat.START)) {
|
|
drawer.closeDrawer(GravityCompat.START);
|
|
return;
|
|
}
|
|
final Fragment fragment = getCurrentFragment();
|
|
if (fragment instanceof OnBackPressedListener) {
|
|
if (!((OnBackPressedListener) fragment).onBackPressed()) {
|
|
super.onBackPressed();
|
|
}
|
|
} else {
|
|
super.onBackPressed();
|
|
}
|
|
Helper.hideKeyboard(this);
|
|
}
|
|
|
|
@Override
|
|
public void onFragmentDone() {
|
|
popFragmentStack(null);
|
|
}
|
|
|
|
@Override
|
|
public SharedPreferences getPrefs() {
|
|
return getPreferences(Context.MODE_PRIVATE);
|
|
}
|
|
|
|
private final List<Integer> accountIds = new ArrayList<>();
|
|
|
|
// generate and cache unique ids for use in accounts list
|
|
private int getAccountId(int accountIndex) {
|
|
final int n = accountIds.size();
|
|
for (int i = n; i <= accountIndex; i++) {
|
|
accountIds.add(View.generateViewId());
|
|
}
|
|
return accountIds.get(accountIndex);
|
|
}
|
|
|
|
// drawer stuff
|
|
|
|
void updateAccountsBalance() {
|
|
final TextView tvBalance = accountsView.getHeaderView(0).findViewById(R.id.tvBalance);
|
|
if (!isStreetMode()) {
|
|
tvBalance.setText(getString(R.string.accounts_balance,
|
|
Helper.getDisplayAmount(getWallet().getBalanceAll(), 5)));
|
|
} else {
|
|
tvBalance.setText(null);
|
|
}
|
|
updateAccountsList();
|
|
}
|
|
|
|
void updateAccountsHeader() {
|
|
final Wallet wallet = getWallet();
|
|
final TextView tvName = accountsView.getHeaderView(0).findViewById(R.id.tvName);
|
|
tvName.setText(wallet.getName());
|
|
}
|
|
|
|
void updateAccountsList() {
|
|
Menu menu = accountsView.getMenu();
|
|
menu.removeGroup(R.id.accounts_list);
|
|
final Wallet wallet = getWallet();
|
|
if (wallet != null) {
|
|
final int n = wallet.getNumAccounts();
|
|
final boolean showBalances = (n > 1) && !isStreetMode();
|
|
for (int i = 0; i < n; i++) {
|
|
final String label = (showBalances ?
|
|
getString(R.string.label_account, wallet.getAccountLabel(i), Helper.getDisplayAmount(wallet.getBalance(i), 2))
|
|
: wallet.getAccountLabel(i));
|
|
final MenuItem item = menu.add(R.id.accounts_list, getAccountId(i), 2 * i, label);
|
|
item.setIcon(R.drawable.ic_account_balance_wallet_black_24dp);
|
|
if (i == wallet.getAccountIndex())
|
|
item.setChecked(true);
|
|
}
|
|
menu.setGroupCheckable(R.id.accounts_list, true, true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setDrawerEnabled(boolean enabled) {
|
|
Timber.d("setDrawerEnabled %b", enabled);
|
|
final int lockMode = enabled ? DrawerLayout.LOCK_MODE_UNLOCKED :
|
|
DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
|
|
drawer.setDrawerLockMode(lockMode);
|
|
drawerToggle.setDrawerIndicatorEnabled(enabled);
|
|
invalidateOptionsMenu(); // menu may need to be changed
|
|
}
|
|
|
|
void updateAccountName() {
|
|
setSubtitle(getWallet().getAccountLabel());
|
|
updateAccountsList();
|
|
}
|
|
|
|
public void onAccountRename() {
|
|
final LayoutInflater li = LayoutInflater.from(this);
|
|
final View promptsView = li.inflate(R.layout.prompt_rename, null);
|
|
|
|
final AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(this);
|
|
alertDialogBuilder.setView(promptsView);
|
|
|
|
final EditText etRename = promptsView.findViewById(R.id.etRename);
|
|
final TextView tvRenameLabel = promptsView.findViewById(R.id.tvRenameLabel);
|
|
final Wallet wallet = getWallet();
|
|
tvRenameLabel.setText(getString(R.string.prompt_rename, wallet.getAccountLabel()));
|
|
|
|
// set dialog message
|
|
alertDialogBuilder
|
|
.setCancelable(false)
|
|
.setPositiveButton(getString(R.string.label_ok),
|
|
(dialog, id) -> {
|
|
Helper.hideKeyboardAlways(WalletActivity.this);
|
|
String newName = etRename.getText().toString();
|
|
wallet.setAccountLabel(newName);
|
|
updateAccountName();
|
|
})
|
|
.setNegativeButton(getString(R.string.label_cancel),
|
|
(dialog, id) -> {
|
|
Helper.hideKeyboardAlways(WalletActivity.this);
|
|
dialog.cancel();
|
|
});
|
|
|
|
final AlertDialog dialog = alertDialogBuilder.create();
|
|
Helper.showKeyboard(dialog);
|
|
|
|
// accept keyboard "ok"
|
|
etRename.setOnEditorActionListener((v, actionId, event) -> {
|
|
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
|
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
|
Helper.hideKeyboardAlways(WalletActivity.this);
|
|
String newName = etRename.getText().toString();
|
|
dialog.cancel();
|
|
wallet.setAccountLabel(newName);
|
|
updateAccountName();
|
|
return false;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
dialog.show();
|
|
}
|
|
|
|
public void setAccountIndex(int accountIndex) {
|
|
getWallet().setAccountIndex(accountIndex);
|
|
selectedSubaddressIndex = 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean onNavigationItemSelected(MenuItem item) {
|
|
final int id = item.getItemId();
|
|
if (id == R.id.account_new) {
|
|
addAccount();
|
|
} else {
|
|
Timber.d("NavigationDrawer ID=%d", id);
|
|
int accountIdx = accountIds.indexOf(id);
|
|
if (accountIdx >= 0) {
|
|
Timber.d("found @%d", accountIdx);
|
|
setAccountIndex(accountIdx);
|
|
}
|
|
forceUpdate();
|
|
drawer.closeDrawer(GravityCompat.START);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private int lastUsedAccount() {
|
|
int lastUsedAccount = 0;
|
|
for (TransactionInfo info : getWallet().getHistory().getAll()) {
|
|
if (info.accountIndex > lastUsedAccount)
|
|
lastUsedAccount = info.accountIndex;
|
|
}
|
|
return lastUsedAccount;
|
|
}
|
|
|
|
private void addAccount() {
|
|
final Wallet wallet = getWallet();
|
|
final int maxAccounts = lastUsedAccount() + wallet.getDeviceType().getAccountLookahead();
|
|
if (wallet.getNumAccounts() < maxAccounts)
|
|
new AsyncAddAccount().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR);
|
|
else
|
|
Toast.makeText(this, getString(R.string.max_account_warning), Toast.LENGTH_LONG).show();
|
|
}
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
private class AsyncAddAccount extends AsyncTask<Void, Void, Boolean> {
|
|
boolean dialogOpened = false;
|
|
|
|
@Override
|
|
protected void onPreExecute() {
|
|
super.onPreExecute();
|
|
switch (getWallet().getDeviceType()) {
|
|
case Device_Ledger:
|
|
showLedgerProgressDialog(LedgerProgressDialog.TYPE_ACCOUNT);
|
|
dialogOpened = true;
|
|
break;
|
|
case Device_Software:
|
|
showProgressDialog(R.string.accounts_progress_new);
|
|
dialogOpened = true;
|
|
break;
|
|
default:
|
|
throw new IllegalStateException("Hardware backing not supported. At all!");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected Boolean doInBackground(Void... params) {
|
|
if (params.length != 0) return false;
|
|
getWallet().addAccount();
|
|
setAccountIndex(getWallet().getNumAccounts() - 1);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Boolean result) {
|
|
super.onPostExecute(result);
|
|
forceUpdate();
|
|
drawer.closeDrawer(GravityCompat.START);
|
|
if (dialogOpened)
|
|
dismissProgressDialog();
|
|
Toast.makeText(WalletActivity.this,
|
|
getString(R.string.accounts_new, getWallet().getNumAccounts() - 1),
|
|
Toast.LENGTH_SHORT).show();
|
|
}
|
|
}
|
|
|
|
// we store the index only and always retrieve a new Subaddress object
|
|
// to ensure we get the current label
|
|
private int selectedSubaddressIndex = 0;
|
|
|
|
@Override
|
|
public Subaddress getSelectedSubaddress() {
|
|
return getWallet().getSubaddressObject(selectedSubaddressIndex);
|
|
}
|
|
|
|
@Override
|
|
public void onSubaddressSelected(@Nullable final Subaddress subaddress) {
|
|
selectedSubaddressIndex = subaddress.getAddressIndex();
|
|
onBackPressed();
|
|
}
|
|
|
|
@Override
|
|
public void showSubaddresses(boolean managerMode) {
|
|
final Bundle b = new Bundle();
|
|
if (managerMode)
|
|
b.putString(SubaddressFragment.KEY_MODE, SubaddressFragment.MODE_MANAGER);
|
|
replaceFragment(new SubaddressFragment(), null, b);
|
|
}
|
|
|
|
@Override
|
|
public void showSubaddress(View view, final int subaddressIndex) {
|
|
final Bundle b = new Bundle();
|
|
b.putInt("subaddressIndex", subaddressIndex);
|
|
replaceFragmentWithTransition(view, new SubaddressInfoFragment(), null, b);
|
|
}
|
|
}
|