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.
Wonerujo/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java

1460 lines
56 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.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
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.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.m2049r.xmrwallet.data.DefaultNodes;
import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.data.NodeInfo;
import com.m2049r.xmrwallet.dialog.AboutFragment;
import com.m2049r.xmrwallet.dialog.CreditsFragment;
import com.m2049r.xmrwallet.dialog.HelpFragment;
import com.m2049r.xmrwallet.dialog.PrivacyFragment;
import com.m2049r.xmrwallet.ledger.Ledger;
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.WalletService;
import com.m2049r.xmrwallet.util.DayNightMode;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
import com.m2049r.xmrwallet.util.LegacyStorageHelper;
import com.m2049r.xmrwallet.util.LocaleHelper;
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
import com.m2049r.xmrwallet.util.NightmodeHelper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import com.m2049r.xmrwallet.util.ZipBackup;
import com.m2049r.xmrwallet.util.ZipRestore;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import timber.log.Timber;
public class LoginActivity extends BaseActivity
implements LoginFragment.Listener, GenerateFragment.Listener,
GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener, NodeFragment.Listener {
private static final String GENERATE_STACK = "gen";
private static final String NODES_PREFS_NAME = "nodes";
private static final String SELECTED_NODE_PREFS_NAME = "selected_node";
private static final String PREF_DAEMON_TESTNET = "daemon_testnet";
private static final String PREF_DAEMON_STAGENET = "daemon_stagenet";
private static final String PREF_DAEMON_MAINNET = "daemon_mainnet";
private NodeInfo node = null;
Set<NodeInfo> favouriteNodes = new HashSet<>();
@Override
public NodeInfo getNode() {
return node;
}
@Override
public void setNode(NodeInfo node) {
setNode(node, true);
}
private void setNode(NodeInfo node, boolean save) {
if (node != this.node) {
if ((node != null) && (node.getNetworkType() != WalletManager.getInstance().getNetworkType()))
throw new IllegalArgumentException("network type does not match");
this.node = node;
for (NodeInfo nodeInfo : favouriteNodes) {
nodeInfo.setSelected(nodeInfo == node);
}
WalletManager.getInstance().setDaemon(node);
if (save)
saveSelectedNode();
}
}
@Override
public Set<NodeInfo> getFavouriteNodes() {
return favouriteNodes;
}
@Override
public Set<NodeInfo> getOrPopulateFavourites() {
if (favouriteNodes.isEmpty()) {
for (DefaultNodes node : DefaultNodes.values()) {
NodeInfo nodeInfo = NodeInfo.fromString(node.getUri());
if (nodeInfo != null) {
nodeInfo.setFavourite(true);
favouriteNodes.add(nodeInfo);
}
}
saveFavourites();
}
return favouriteNodes;
}
@Override
public void setFavouriteNodes(Collection<NodeInfo> nodes) {
Timber.d("adding %d nodes", nodes.size());
favouriteNodes.clear();
for (NodeInfo node : nodes) {
Timber.d("adding %s %b", node, node.isFavourite());
if (node.isFavourite())
favouriteNodes.add(node);
}
saveFavourites();
}
private void loadFavouritesWithNetwork() {
Helper.runWithNetwork(() -> {
loadFavourites();
return true;
});
}
private void loadFavourites() {
Timber.d("loadFavourites");
favouriteNodes.clear();
final String selectedNodeId = getSelectedNodeId();
Map<String, ?> storedNodes = getSharedPreferences(NODES_PREFS_NAME, Context.MODE_PRIVATE).getAll();
for (Map.Entry<String, ?> nodeEntry : storedNodes.entrySet()) {
if (nodeEntry != null) { // just in case, ignore possible future errors
final String nodeId = (String) nodeEntry.getValue();
final NodeInfo addedNode = addFavourite(nodeId);
if (addedNode != null) {
if (nodeId.equals(selectedNodeId)) {
addedNode.setSelected(true);
}
}
}
}
if (storedNodes.isEmpty()) { // try to load legacy list & remove it (i.e. migrate the data once)
SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
switch (WalletManager.getInstance().getNetworkType()) {
case NetworkType_Mainnet:
loadLegacyList(sharedPref.getString(PREF_DAEMON_MAINNET, null));
sharedPref.edit().remove(PREF_DAEMON_MAINNET).apply();
break;
case NetworkType_Stagenet:
loadLegacyList(sharedPref.getString(PREF_DAEMON_STAGENET, null));
sharedPref.edit().remove(PREF_DAEMON_STAGENET).apply();
break;
case NetworkType_Testnet:
loadLegacyList(sharedPref.getString(PREF_DAEMON_TESTNET, null));
sharedPref.edit().remove(PREF_DAEMON_TESTNET).apply();
break;
default:
throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType());
}
}
}
private void saveFavourites() {
Timber.d("SAVE");
SharedPreferences.Editor editor = getSharedPreferences(NODES_PREFS_NAME, Context.MODE_PRIVATE).edit();
editor.clear();
int i = 1;
for (Node info : favouriteNodes) {
final String nodeString = info.toNodeString();
editor.putString(Integer.toString(i), nodeString);
Timber.d("saved %d:%s", i, nodeString);
i++;
}
editor.apply();
}
private NodeInfo addFavourite(String nodeString) {
final NodeInfo nodeInfo = NodeInfo.fromString(nodeString);
if (nodeInfo != null) {
nodeInfo.setFavourite(true);
favouriteNodes.add(nodeInfo);
} else
Timber.w("nodeString invalid: %s", nodeString);
return nodeInfo;
}
private void loadLegacyList(final String legacyListString) {
if (legacyListString == null) return;
final String[] nodeStrings = legacyListString.split(";");
for (final String nodeString : nodeStrings) {
addFavourite(nodeString);
}
}
private void saveSelectedNode() { // save only if changed
final NodeInfo nodeInfo = getNode();
final String selectedNodeId = getSelectedNodeId();
if (nodeInfo != null) {
if (!nodeInfo.toNodeString().equals(selectedNodeId))
saveSelectedNode(nodeInfo);
} else {
if (selectedNodeId != null)
saveSelectedNode(null);
}
}
private void saveSelectedNode(NodeInfo nodeInfo) {
SharedPreferences.Editor editor = getSharedPreferences(SELECTED_NODE_PREFS_NAME, Context.MODE_PRIVATE).edit();
if (nodeInfo == null) {
editor.clear();
} else {
editor.putString("0", getNode().toNodeString());
}
editor.apply();
}
private String getSelectedNodeId() {
return getSharedPreferences(SELECTED_NODE_PREFS_NAME, Context.MODE_PRIVATE)
.getString("0", null);
}
private Toolbar toolbar;
@Override
public void setToolbarButton(int type) {
toolbar.setButton(type);
}
@Override
public void setTitle(String title) {
toolbar.setTitle(title);
}
@Override
public void setSubtitle(String subtitle) {
toolbar.setSubtitle(subtitle);
}
@Override
public void setTitle(String title, String subtitle) {
toolbar.setTitle(title, subtitle);
}
@Override
public boolean hasLedger() {
return Ledger.isConnected();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Timber.d("onCreate()");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayShowTitleEnabled(false);
toolbar.setOnButtonListener(type -> {
switch (type) {
case Toolbar.BUTTON_BACK:
onBackPressed();
break;
case Toolbar.BUTTON_CLOSE:
finish();
break;
case Toolbar.BUTTON_CREDITS:
CreditsFragment.display(getSupportFragmentManager());
break;
case Toolbar.BUTTON_NONE:
break;
default:
Timber.e("Button " + type + "pressed - how can this be?");
}
});
loadFavouritesWithNetwork();
LegacyStorageHelper.migrateWallets(this);
if (savedInstanceState == null) startLoginFragment();
// try intents
Intent intent = getIntent();
if (!processUsbIntent(intent))
processUriIntent(intent);
}
boolean checkServiceRunning() {
if (WalletService.Running) {
Toast.makeText(this, getString(R.string.service_busy), Toast.LENGTH_SHORT).show();
return true;
} else {
return false;
}
}
@Override
public boolean onWalletSelected(String walletName, boolean streetmode) {
if (node == null) {
Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show();
return false;
}
if (checkServiceRunning()) return false;
try {
new AsyncOpenWallet(walletName, node, streetmode).execute();
} catch (IllegalArgumentException ex) {
Timber.e(ex.getLocalizedMessage());
Toast.makeText(this, ex.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
return false;
}
return true;
}
@Override
public void onWalletDetails(final String walletName) {
Timber.d("details for wallet .%s.", walletName);
if (checkServiceRunning()) return;
DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
final File walletFile = Helper.getWalletFile(LoginActivity.this, walletName);
if (WalletManager.getInstance().walletExists(walletFile)) {
Helper.promptPassword(LoginActivity.this, walletName, true, new Helper.PasswordAction() {
@Override
public void act(String walletName1, String password, boolean fingerprintUsed) {
if (checkDevice(walletName1, password))
startDetails(walletFile, password, GenerateReviewFragment.VIEW_TYPE_DETAILS);
}
@Override
public void fail(String walletName) {
}
});
} else { // this cannot really happen as we prefilter choices
Timber.e("Wallet missing: %s", walletName);
Toast.makeText(LoginActivity.this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show();
}
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();
}
private void renameWallet(String oldName, String newName) {
File walletFile = Helper.getWalletFile(this, oldName);
boolean success = renameWallet(walletFile, newName);
if (success) {
reloadWalletList();
} else {
Toast.makeText(LoginActivity.this, getString(R.string.rename_failed), Toast.LENGTH_LONG).show();
}
}
// copy + delete seems safer than rename because we can rollback easily
boolean renameWallet(File walletFile, String newName) {
if (copyWallet(walletFile, new File(walletFile.getParentFile(), newName), false, true)) {
try {
KeyStoreHelper.copyWalletUserPass(this, walletFile.getName(), newName);
} catch (KeyStoreHelper.BrokenPasswordStoreException ex) {
Timber.w(ex);
}
deleteWallet(walletFile); // also deletes the keystore entry
return true;
} else {
return false;
}
}
@Override
public void onWalletRename(final String walletName) {
Timber.d("rename for wallet ." + walletName + ".");
if (checkServiceRunning()) return;
LayoutInflater li = LayoutInflater.from(this);
View promptsView = li.inflate(R.layout.prompt_rename, null);
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);
tvRenameLabel.setText(getString(R.string.prompt_rename, walletName));
// set dialog message
alertDialogBuilder
.setCancelable(false)
.setPositiveButton(getString(R.string.label_ok),
(dialog, id) -> {
Helper.hideKeyboardAlways(LoginActivity.this);
String newName = etRename.getText().toString();
renameWallet(walletName, newName);
})
.setNegativeButton(getString(R.string.label_cancel),
(dialog, id) -> {
Helper.hideKeyboardAlways(LoginActivity.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(LoginActivity.this);
String newName = etRename.getText().toString();
dialog.cancel();
renameWallet(walletName, newName);
return false;
}
return false;
});
dialog.show();
}
private static final int CREATE_BACKUP_INTENT = 4711;
private static final int RESTORE_BACKUP_INTENT = 4712;
private ZipBackup zipBackup;
@Override
public void onWalletBackup(String walletName) {
Timber.d("backup for wallet ." + walletName + ".");
// overwrite any pending backup request
zipBackup = new ZipBackup(this, walletName);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_TITLE, zipBackup.getBackupName());
startActivityForResult(intent, CREATE_BACKUP_INTENT);
}
@Override
public void onWalletRestore() {
Timber.d("restore wallet");
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/zip");
startActivityForResult(intent, RESTORE_BACKUP_INTENT);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CREATE_BACKUP_INTENT) {
if (data == null) {
// nothing selected
Toast.makeText(this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show();
zipBackup = null;
return;
}
try {
if (zipBackup == null) return; // ignore unsolicited request
zipBackup.writeTo(data.getData());
Toast.makeText(this, getString(R.string.backup_success), Toast.LENGTH_SHORT).show();
} catch (IOException ex) {
Timber.e(ex);
Toast.makeText(this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show();
} finally {
zipBackup = null;
}
} else if (requestCode == RESTORE_BACKUP_INTENT) {
if (data == null) {
// nothing selected
Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show();
return;
}
try {
ZipRestore zipRestore = new ZipRestore(this, data.getData());
Toast.makeText(this, getString(R.string.menu_restore), Toast.LENGTH_SHORT).show();
if (zipRestore.restore()) {
reloadWalletList();
} else {
Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show();
}
} catch (IOException ex) {
Timber.e(ex);
Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show();
}
}
}
@Override
public void onWalletDelete(final String walletName) {
Timber.d("delete for wallet ." + walletName + ".");
if (checkServiceRunning()) return;
DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
if (deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) {
reloadWalletList();
} else {
Toast.makeText(LoginActivity.this, getString(R.string.delete_failed), Toast.LENGTH_LONG).show();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
// do nothing
break;
}
};
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setMessage(getString(R.string.delete_alert_message))
.setTitle(walletName)
.setPositiveButton(getString(R.string.delete_alert_yes), dialogClickListener)
.setNegativeButton(getString(R.string.delete_alert_no), dialogClickListener)
.show();
}
@Override
public void onWalletDeleteCache(final String walletName) {
Timber.d("delete cache for wallet ." + walletName + ".");
if (checkServiceRunning()) return;
DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
if (!deleteWalletCache(Helper.getWalletFile(LoginActivity.this, walletName))) {
Toast.makeText(LoginActivity.this, getString(R.string.delete_failed), Toast.LENGTH_LONG).show();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
// do nothing
break;
}
};
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setMessage(getString(R.string.deletecache_alert_message, walletName))
.setTitle(walletName)
.setPositiveButton(getString(R.string.delete_alert_yes), dialogClickListener)
.setNegativeButton(getString(R.string.delete_alert_no), dialogClickListener)
.show();
}
void reloadWalletList() {
Timber.d("reloadWalletList()");
try {
LoginFragment loginFragment = (LoginFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (loginFragment != null) {
loginFragment.loadList();
}
} catch (ClassCastException ex) {
Timber.w(ex);
}
}
public void onWalletChangePassword() {//final String walletName, final String walletPassword) {
try {
GenerateReviewFragment detailsFragment = (GenerateReviewFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
if (dialog != null) {
Helper.showKeyboard(dialog);
dialog.show();
}
} catch (ClassCastException ex) {
Timber.w("onWalletChangePassword() called, but no GenerateReviewFragment active");
}
}
@Override
public void onAddWallet(String type) {
if (checkServiceRunning()) return;
startGenerateFragment(type);
}
@Override
public void onNodePrefs() {
Timber.d("node prefs");
if (checkServiceRunning()) return;
startNodeFragment();
}
////////////////////////////////////////
// LoginFragment.Listener
////////////////////////////////////////
@Override
public File getStorageRoot() {
return Helper.getWalletRoot(getApplicationContext());
}
////////////////////////////////////////
////////////////////////////////////////
@Override
public void showNet() {
showNet(WalletManager.getInstance().getNetworkType());
}
private void showNet(NetworkType net) {
switch (net) {
case NetworkType_Mainnet:
toolbar.setSubtitle(null);
toolbar.setBackgroundResource(R.drawable.backgound_toolbar_mainnet);
break;
case NetworkType_Testnet:
toolbar.setSubtitle(getString(R.string.connect_testnet));
toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark));
break;
case NetworkType_Stagenet:
toolbar.setSubtitle(getString(R.string.connect_stagenet));
toolbar.setBackgroundResource(ThemeHelper.getThemedResourceId(this, R.attr.colorPrimaryDark));
break;
default:
throw new IllegalStateException("NetworkType unknown: " + net);
}
}
@Override
protected void onPause() {
Timber.d("onPause()");
super.onPause();
}
@Override
protected void onDestroy() {
Timber.d("onDestroy");
dismissProgressDialog();
unregisterDetachReceiver();
Ledger.disconnect();
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
Timber.d("onResume()");
// wait for WalletService to finish
if (WalletService.Running && (progressDialog == null)) {
// and show a progress dialog, but only if there isn't one already
new AsyncWaitForService().execute();
}
if (!Ledger.isConnected()) attachLedger();
}
private class AsyncWaitForService extends AsyncTask<Void, Void, Void> {
@Override
protected void onPreExecute() {
super.onPreExecute();
showProgressDialog(R.string.service_progress);
}
@Override
protected Void doInBackground(Void... params) {
try {
while (WalletService.Running & !isCancelled()) {
Thread.sleep(250);
}
} catch (InterruptedException ex) {
// oh well ...
}
return null;
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
if (isDestroyed()) {
return;
}
dismissProgressDialog();
}
}
void startWallet(String walletName, String walletPassword,
boolean fingerprintUsed, boolean streetmode) {
Timber.d("startWallet()");
Intent intent = new Intent(getApplicationContext(), WalletActivity.class);
intent.putExtra(WalletActivity.REQUEST_ID, walletName);
intent.putExtra(WalletActivity.REQUEST_PW, walletPassword);
intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed);
intent.putExtra(WalletActivity.REQUEST_STREETMODE, streetmode);
if (uri != null) {
intent.putExtra(WalletActivity.REQUEST_URI, uri);
uri = null; // use only once
}
startActivity(intent);
}
void startDetails(File walletFile, String password, String type) {
Timber.d("startDetails()");
Bundle b = new Bundle();
b.putString("path", walletFile.getAbsolutePath());
b.putString("password", password);
b.putString("type", type);
startReviewFragment(b);
}
void startLoginFragment() {
// we set these here because we cannot be ceratin we have permissions for storage before
Helper.setMoneroHome(this);
Helper.initLogger(this);
Fragment fragment = new LoginFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, fragment).commit();
Timber.d("LoginFragment added");
}
void startGenerateFragment(String type) {
Bundle extras = new Bundle();
extras.putString(GenerateFragment.TYPE, type);
replaceFragment(new GenerateFragment(), GENERATE_STACK, extras);
Timber.d("GenerateFragment placed");
}
void startReviewFragment(Bundle extras) {
replaceFragment(new GenerateReviewFragment(), null, extras);
Timber.d("GenerateReviewFragment placed");
}
void startNodeFragment() {
replaceFragment(new NodeFragment(), null, null);
Timber.d("NodeFragment placed");
}
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
if (extras != null) {
newFragment.setArguments(extras);
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(stackName);
transaction.commit();
}
void popFragmentStack(String name) {
getSupportFragmentManager().popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
//////////////////////////////////////////
// GenerateFragment.Listener
//////////////////////////////////////////
static final String MNEMONIC_LANGUAGE = "English"; // see mnemonics/electrum-words.cpp for more
private class AsyncCreateWallet extends AsyncTask<Void, Void, Boolean> {
final String walletName;
final String walletPassword;
final WalletCreator walletCreator;
File newWalletFile;
AsyncCreateWallet(final String name, final String password,
final WalletCreator walletCreator) {
super();
this.walletName = name;
this.walletPassword = password;
this.walletCreator = walletCreator;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
acquireWakeLock();
if (walletCreator.isLedger()) {
showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
} else {
showProgressDialog(R.string.generate_wallet_creating);
}
}
@Override
protected Boolean doInBackground(Void... params) {
// check if the wallet we want to create already exists
File walletFolder = getStorageRoot();
if (!walletFolder.isDirectory()) {
Timber.e("Wallet dir " + walletFolder.getAbsolutePath() + "is not a directory");
return false;
}
File cacheFile = new File(walletFolder, walletName);
File keysFile = new File(walletFolder, walletName + ".keys");
File addressFile = new File(walletFolder, walletName + ".address.txt");
if (cacheFile.exists() || keysFile.exists() || addressFile.exists()) {
Timber.e("Some wallet files already exist for %s", cacheFile.getAbsolutePath());
return false;
}
newWalletFile = new File(walletFolder, walletName);
boolean success = walletCreator.createWallet(newWalletFile, walletPassword);
if (success) {
return true;
} else {
Timber.e("Could not create new wallet in %s", newWalletFile.getAbsolutePath());
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
releaseWakeLock(RELEASE_WAKE_LOCK_DELAY);
if (isDestroyed()) {
return;
}
dismissProgressDialog();
if (result) {
startDetails(newWalletFile, walletPassword, GenerateReviewFragment.VIEW_TYPE_ACCEPT);
} else {
walletGenerateError();
}
}
}
public void createWallet(final String name, final String password,
final WalletCreator walletCreator) {
new AsyncCreateWallet(name, password, walletCreator)
.executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR);
}
void walletGenerateError() {
try {
GenerateFragment genFragment = (GenerateFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
genFragment.walletGenerateError();
} catch (ClassCastException ex) {
Timber.e("walletGenerateError() but not in GenerateFragment");
}
}
interface WalletCreator {
boolean createWallet(File aFile, String password);
boolean isLedger();
}
boolean checkAndCloseWallet(Wallet aWallet) {
Wallet.Status walletStatus = aWallet.getStatus();
if (!walletStatus.isOk()) {
Timber.e(walletStatus.getErrorString());
toast(walletStatus.getErrorString());
}
aWallet.close();
return walletStatus.isOk();
}
@Override
public void onGenerate(final String name, final String password) {
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return false;
}
@Override
public boolean createWallet(File aFile, String password) {
NodeInfo currentNode = getNode();
// get it from the connected node if we have one, and go back ca. 4 days
final long restoreHeight =
(currentNode != null) ? currentNode.getHeight() - 2000 : -1;
Wallet newWallet = WalletManager.getInstance()
.createWallet(aFile, password, MNEMONIC_LANGUAGE, restoreHeight);
return checkAndCloseWallet(newWallet);
}
});
}
@Override
public void onGenerate(final String name, final String password, final String seed,
final long restoreHeight) {
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return false;
}
@Override
public boolean createWallet(File aFile, String password) {
Wallet newWallet = WalletManager.getInstance()
.recoveryWallet(aFile, password, seed, restoreHeight);
return checkAndCloseWallet(newWallet);
}
});
}
@Override
public void onGenerateLedger(final String name, final String password,
final long restoreHeight) {
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return true;
}
@Override
public boolean createWallet(File aFile, String password) {
Wallet newWallet = WalletManager.getInstance()
.createWalletFromDevice(aFile, password,
restoreHeight, "Ledger");
return checkAndCloseWallet(newWallet);
}
});
}
@Override
public void onGenerate(final String name, final String password,
final String address, final String viewKey, final String spendKey,
final long restoreHeight) {
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return false;
}
@Override
public boolean createWallet(File aFile, String password) {
Wallet newWallet = WalletManager.getInstance()
.createWalletWithKeys(aFile, password, MNEMONIC_LANGUAGE, restoreHeight,
address, viewKey, spendKey);
return checkAndCloseWallet(newWallet);
}
});
}
private void toast(final String msg) {
runOnUiThread(() -> Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_LONG).show());
}
private void toast(final int msgId) {
runOnUiThread(() -> Toast.makeText(LoginActivity.this, getString(msgId), Toast.LENGTH_LONG).show());
}
@Override
public void onAccept(final String name, final String password) {
File walletFolder = getStorageRoot();
File walletFile = new File(walletFolder, name);
Timber.d("New Wallet %s", walletFile.getAbsolutePath());
walletFile.delete(); // when recovering wallets, the cache seems corrupt - so remove it
popFragmentStack(GENERATE_STACK);
Toast.makeText(LoginActivity.this,
getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show();
}
boolean walletExists(File walletFile, boolean any) {
File dir = walletFile.getParentFile();
String name = walletFile.getName();
if (any) {
return new File(dir, name).exists()
|| new File(dir, name + ".keys").exists()
|| new File(dir, name + ".address.txt").exists();
} else {
return new File(dir, name).exists()
&& new File(dir, name + ".keys").exists()
&& new File(dir, name + ".address.txt").exists();
}
}
boolean copyWallet(File srcWallet, File dstWallet, boolean overwrite, boolean ignoreCacheError) {
if (walletExists(dstWallet, true) && !overwrite) return false;
boolean success = false;
File srcDir = srcWallet.getParentFile();
String srcName = srcWallet.getName();
File dstDir = dstWallet.getParentFile();
String dstName = dstWallet.getName();
try {
copyFile(new File(srcDir, srcName + ".keys"), new File(dstDir, dstName + ".keys"));
try { // cache & address.txt are optional files
copyFile(new File(srcDir, srcName), new File(dstDir, dstName));
copyFile(new File(srcDir, srcName + ".address.txt"), new File(dstDir, dstName + ".address.txt"));
} catch (IOException ex) {
Timber.d("CACHE %s", ignoreCacheError);
if (!ignoreCacheError) { // ignore cache backup error if backing up (can be resynced)
throw ex;
}
}
success = true;
} catch (IOException ex) {
Timber.e("wallet copy failed: %s", ex.getMessage());
// try to rollback
deleteWallet(dstWallet);
}
return success;
}
// do our best to delete as much as possible of the wallet files
boolean deleteWallet(File walletFile) {
Timber.d("deleteWallet %s", walletFile.getAbsolutePath());
File dir = walletFile.getParentFile();
String name = walletFile.getName();
boolean success = true;
File cacheFile = new File(dir, name);
if (cacheFile.exists()) {
success = cacheFile.delete();
}
success = new File(dir, name + ".keys").delete() && success;
File addressFile = new File(dir, name + ".address.txt");
if (addressFile.exists()) {
success = addressFile.delete() && success;
}
Timber.d("deleteWallet is %s", success);
KeyStoreHelper.removeWalletUserPass(this, walletFile.getName());
return success;
}
boolean deleteWalletCache(File walletFile) {
Timber.d("deleteWalletCache %s", walletFile.getAbsolutePath());
File dir = walletFile.getParentFile();
String name = walletFile.getName();
boolean success = true;
File cacheFile = new File(dir, name);
if (cacheFile.exists()) {
success = cacheFile.delete();
}
return success;
}
void copyFile(File src, File dst) throws IOException {
try (FileChannel inChannel = new FileInputStream(src).getChannel();
FileChannel outChannel = new FileOutputStream(dst).getChannel()) {
inChannel.transferTo(0, inChannel.size(), outChannel);
}
}
public void onChangeLocale() {
final ArrayList<Locale> availableLocales = LocaleHelper.getAvailableLocales(LoginActivity.this);
Collections.sort(availableLocales, (locale1, locale2) -> {
String localeString1 = LocaleHelper.getDisplayName(locale1, true);
String localeString2 = LocaleHelper.getDisplayName(locale2, true);
return localeString1.compareTo(localeString2);
});
String[] localeDisplayNames = new String[1 + availableLocales.size()];
localeDisplayNames[0] = getString(R.string.language_system_default);
for (int i = 1; i < localeDisplayNames.length; i++) {
localeDisplayNames[i] = LocaleHelper.getDisplayName(availableLocales.get(i - 1), true);
}
int currentLocaleIndex = 0;
String currentLocaleTag = LocaleHelper.getPreferredLanguageTag(LoginActivity.this);
if (!currentLocaleTag.isEmpty()) {
Locale currentLocale = Locale.forLanguageTag(currentLocaleTag);
String currentLocaleName = LocaleHelper.getDisplayName(currentLocale, true);
currentLocaleIndex = Arrays.asList(localeDisplayNames).indexOf(currentLocaleName);
if (currentLocaleIndex < 0) currentLocaleIndex = 0;
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(LoginActivity.this);
builder.setTitle(getString(R.string.menu_language));
builder.setSingleChoiceItems(localeDisplayNames, currentLocaleIndex, (dialog, i) -> {
dialog.dismiss();
LocaleHelper.setAndSaveLocale(this,
(i == 0) ? "" : availableLocales.get(i - 1).toLanguageTag());
startActivity(getIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK));
});
builder.show();
}
public void onChangeTheme() {
final DayNightMode currentDayNightSetting = DayNightMode.getValue(AppCompatDelegate.getDefaultNightMode());
// selection will be empty if UNKNOWN
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(LoginActivity.this);
builder.setTitle(getString(R.string.menu_daynight));
String[] modeNames = getResources().getStringArray(R.array.daynight_themes);
builder.setSingleChoiceItems(modeNames, currentDayNightSetting.ordinal(), (dialog, i) -> {
dialog.dismiss();
final DayNightMode mode = DayNightMode.values()[i];
if (currentDayNightSetting != mode) {
NightmodeHelper.setAndSavePreferredNightmode(LoginActivity.this, mode);
LoginActivity.this.recreate();
}
});
builder.show();
}
@Override
public void onBackPressed() {
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (f instanceof GenerateReviewFragment) {
if (((GenerateReviewFragment) f).backOk()) {
super.onBackPressed();
}
} else if (f instanceof NodeFragment) {
if (!((NodeFragment) f).isRefreshing()) {
super.onBackPressed();
} else {
Toast.makeText(LoginActivity.this, getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show();
}
} else if (f instanceof LoginFragment) {
if (((LoginFragment) f).isFabOpen()) {
((LoginFragment) f).animateFAB();
} else {
super.onBackPressed();
}
} else {
super.onBackPressed();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int id = item.getItemId();
if (id == R.id.action_create_help_new) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_new);
return true;
} else if (id == R.id.action_create_help_keys) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_keys);
return true;
} else if (id == R.id.action_create_help_view) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_view);
return true;
} else if (id == R.id.action_create_help_seed) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_seed);
return true;
} else if (id == R.id.action_create_help_ledger) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger);
return true;
} else if (id == R.id.action_details_help) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_details);
return true;
} else if (id == R.id.action_details_changepw) {
onWalletChangePassword();
return true;
} else if (id == R.id.action_license_info) {
AboutFragment.display(getSupportFragmentManager());
return true;
} else if (id == R.id.action_help_list) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_list);
return true;
} else if (id == R.id.action_help_node) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_node);
return true;
} else if (id == R.id.action_default_nodes) {
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) &&
(f instanceof NodeFragment)) {
((NodeFragment) f).restoreDefaultNodes();
}
return true;
} else if (id == R.id.action_privacy_policy) {
PrivacyFragment.display(getSupportFragmentManager());
return true;
} else if (id == R.id.action_language) {
onChangeLocale();
return true;
} else if (id == R.id.action_theme) {
onChangeTheme();
return true;
} else if (id == R.id.action_restore) {
onWalletRestore();
return true;
} else if (id == R.id.action_ledger_seed) {
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (f instanceof GenerateFragment) {
((GenerateFragment) f).convertLedgerSeed();
}
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
// an AsyncTask which tests the node before trying to open the wallet
private class AsyncOpenWallet extends AsyncTask<Void, Void, Boolean> {
final static int OK = 0;
final static int TIMEOUT = 1;
final static int INVALID = 2;
final static int IOEX = 3;
private final String walletName;
private final NodeInfo node;
private final boolean streetmode;
AsyncOpenWallet(String walletName, NodeInfo node, boolean streetmode) {
this.walletName = walletName;
this.node = node;
this.streetmode = streetmode;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Boolean doInBackground(Void... params) {
Timber.d("checking %s", node.getAddress());
return node.testRpcService();
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (isDestroyed()) {
return;
}
if (result) {
Timber.d("selected wallet is .%s.", node.getName());
// now it's getting real, onValidateFields if wallet exists
promptAndStart(walletName, streetmode);
} else {
if (node.getResponseCode() == 0) { // IOException
Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show();
} else { // connected but broken
Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_connect_ioex), Toast.LENGTH_LONG).show();
}
}
}
}
boolean checkDevice(String walletName, String password) {
String keyPath = new File(Helper.getWalletRoot(LoginActivity.this),
walletName + ".keys").getAbsolutePath();
// check if we need connected hardware
Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password);
if (device == Wallet.Device.Device_Ledger) {
if (!hasLedger()) {
toast(R.string.open_wallet_ledger_missing);
} else {
return true;
}
} else {// device could be undefined meaning the password is wrong
// this gets dealt with later
return true;
}
return false;
}
void promptAndStart(String walletName, final boolean streetmode) {
File walletFile = Helper.getWalletFile(this, walletName);
if (WalletManager.getInstance().walletExists(walletFile)) {
Helper.promptPassword(LoginActivity.this, walletName, false,
new Helper.PasswordAction() {
@Override
public void act(String walletName, String password, boolean fingerprintUsed) {
if (checkDevice(walletName, password))
startWallet(walletName, password, fingerprintUsed, streetmode);
}
@Override
public void fail(String walletName) {
}
});
} else { // this cannot really happen as we prefilter choices
Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show();
}
}
// USB Stuff - (Ledger)
private static final String ACTION_USB_PERMISSION = "com.m2049r.xmrwallet.USB_PERMISSION";
void attachLedger() {
final UsbManager usbManager = getUsbManager();
UsbDevice device = Ledger.findDevice(usbManager);
if (device != null) {
if (usbManager.hasPermission(device)) {
connectLedger(usbManager, device);
} else {
registerReceiver(usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION));
usbManager.requestPermission(device,
PendingIntent.getBroadcast(this, 0,
new Intent(ACTION_USB_PERMISSION), 0));
}
} else {
Timber.d("no ledger device found");
}
}
private final BroadcastReceiver usbPermissionReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
unregisterReceiver(usbPermissionReceiver);
synchronized (this) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
if (device != null) {
connectLedger(getUsbManager(), device);
}
} else {
Timber.w("User denied permission for device %s", device.getProductName());
}
}
}
}
};
private void connectLedger(UsbManager usbManager, final UsbDevice usbDevice) {
if (Ledger.ENABLED)
try {
Ledger.connect(usbManager, usbDevice);
if (!Ledger.check()) {
Ledger.disconnect();
runOnUiThread(() -> Toast.makeText(LoginActivity.this,
getString(R.string.toast_ledger_start_app, usbDevice.getProductName()),
Toast.LENGTH_SHORT)
.show());
} else {
registerDetachReceiver();
onLedgerAction();
runOnUiThread(() -> Toast.makeText(LoginActivity.this,
getString(R.string.toast_ledger_attached, usbDevice.getProductName()),
Toast.LENGTH_SHORT)
.show());
}
} catch (IOException ex) {
runOnUiThread(() -> Toast.makeText(LoginActivity.this,
getString(R.string.open_wallet_ledger_missing),
Toast.LENGTH_SHORT)
.show());
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
processUsbIntent(intent);
}
private boolean processUsbIntent(Intent intent) {
String action = intent.getAction();
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
synchronized (this) {
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (device != null) {
final UsbManager usbManager = getUsbManager();
if (usbManager.hasPermission(device)) {
Timber.d("Ledger attached by intent");
connectLedger(usbManager, device);
}
}
}
return true;
}
return false;
}
private String uri = null;
private void processUriIntent(Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action)) {
synchronized (this) {
uri = intent.getDataString();
Timber.d("URI Intent %s", uri);
HelpFragment.display(getSupportFragmentManager(), R.string.help_uri);
}
}
}
BroadcastReceiver detachReceiver;
private void unregisterDetachReceiver() {
if (detachReceiver != null) {
unregisterReceiver(detachReceiver);
detachReceiver = null;
}
}
private void registerDetachReceiver() {
detachReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
unregisterDetachReceiver();
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
Timber.i("Ledger detached!");
if (device != null)
runOnUiThread(() -> Toast.makeText(LoginActivity.this,
getString(R.string.toast_ledger_detached, device.getProductName()),
Toast.LENGTH_SHORT)
.show());
Ledger.disconnect();
onLedgerAction();
}
}
};
registerReceiver(detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED));
}
public void onLedgerAction() {
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (f instanceof GenerateFragment) {
onBackPressed();
} else if (f instanceof LoginFragment) {
if (((LoginFragment) f).isFabOpen()) {
((LoginFragment) f).animateFAB();
}
}
}
// get UsbManager or die trying
@NonNull
private UsbManager getUsbManager() {
final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
if (usbManager == null) {
throw new IllegalStateException("no USB_SERVICE");
}
return usbManager;
}
}