Merge pull request #1 from m2049r/master

Merging Up to date
This commit is contained in:
KeeJef 2017-09-07 14:08:27 +10:00 committed by GitHub
commit c9bbe1db3f
32 changed files with 1436 additions and 257 deletions

2
.idea/.gitignore vendored
View File

@ -1,2 +1,2 @@
workspace.xml
markdown-navigator*
markdown-*

View File

@ -21,7 +21,6 @@ You may loose all your Moneroj if you use this App. Be cautious when spending on
- Monerujo means "Monero Wallet" according to https://www.reddit.com/r/Monero/comments/3exy7t/esperanto_corner/
### TODO
- wallet backup functions
- review visibility of methods/classes
- more sensible error dialogs
- check licenses of included libraries; License Dialog

View File

@ -7,8 +7,8 @@ android {
applicationId "com.m2049r.xmrwallet"
minSdkVersion 21
targetSdkVersion 25
versionCode 10
versionName "0.5.3"
versionCode 11
versionName "0.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {

View File

@ -11,7 +11,7 @@
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/ic_monero_32dp"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">

View File

@ -461,7 +461,6 @@ Java_com_m2049r_xmrwallet_model_WalletManager_resolveOpenAlias(JNIEnv *env, jobj
//TODO static std::tuple<bool, std::string, std::string, std::string, std::string> checkUpdates(const std::string &software, const std::string &subdir);
// actually a WalletManager function, but logically in onWalletSelected
JNIEXPORT jboolean JNICALL
Java_com_m2049r_xmrwallet_model_WalletManager_closeJ(JNIEnv *env, jobject instance,
jobject walletInstance) {

View File

@ -69,12 +69,9 @@ public class GenerateFragment extends Fragment {
bGenerate = (Button) view.findViewById(R.id.bGenerate);
etWalletMnemonic.setRawInputType(InputType.TYPE_CLASS_TEXT);
etWalletAddress.setRawInputType(InputType.TYPE_CLASS_TEXT);
etWalletViewKey.setRawInputType(InputType.TYPE_CLASS_TEXT);
etWalletSpendKey.setRawInputType(InputType.TYPE_CLASS_TEXT);
boolean testnet = WalletManager.getInstance().isTestNet();
//etWalletMnemonic.setTextIsSelectable(testnet);
etWalletAddress.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etWalletViewKey.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etWalletSpendKey.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
Helper.showKeyboard(getActivity());
etWalletName.addTextChangedListener(new TextWatcher() {
@ -298,8 +295,12 @@ public class GenerateFragment extends Fragment {
private void generateWallet() {
String name = etWalletName.getText().toString();
if (name.length() == 0) return;
String walletPath = Helper.getWalletPath(getActivity(), name);
if (WalletManager.getInstance().walletExists(walletPath)) {
if (name.charAt(0)=='.') {
Toast.makeText(getActivity(), getString(R.string.generate_wallet_dot), Toast.LENGTH_LONG).show();
etWalletName.requestFocus();
}
File walletFile = Helper.getWalletFile(getActivity(), name);
if (WalletManager.getInstance().walletExists(walletFile)) {
Toast.makeText(getActivity(), getString(R.string.generate_wallet_exists), Toast.LENGTH_LONG).show();
etWalletName.requestFocus();
return;
@ -348,7 +349,7 @@ public class GenerateFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onPause()");
Log.d(TAG, "onResume()");
activityCallback.setTitle(getString(R.string.generate_title));
}

View File

@ -17,8 +17,10 @@
package com.m2049r.xmrwallet;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -28,15 +30,13 @@ import android.widget.TextView;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.MoneroHandlerThread;
import java.io.File;
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
public class GenerateReviewFragment extends Fragment {
static final String TAG = "GenerateReviewFragment";
static final public String VIEW_DETAILS = "details";
static final public String VIEW_ACCEPT = "accept";
static final public String VIEW_WALLET = "wallet";
static final public String VIEW_TYPE_DETAILS = "details";
static final public String VIEW_TYPE_ACCEPT = "accept";
static final public String VIEW_TYPE_WALLET = "wallet";
ProgressBar pbProgress;
TextView tvWalletName;
@ -76,16 +76,12 @@ public class GenerateReviewFragment extends Fragment {
showProgress();
Bundle b = getArguments();
String type = b.getString("type");
if (!type.equals(VIEW_WALLET)) {
String name = b.getString("name");
String password = b.getString("password");
tvWalletName.setText(new File(name).getName());
show(name, password, type);
} else {
show(walletCallback.getWallet(), null, type);
}
Bundle args = getArguments();
String path = args.getString("path");
String password = args.getString("password");
String type = args.getString("type");
new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR,
path, password, type);
return view;
}
@ -96,41 +92,65 @@ public class GenerateReviewFragment extends Fragment {
acceptCallback.onAccept(name, password);
}
private void show(final String walletPath, final String password, final String type) {
new Thread(null,
new Runnable() {
private class AsyncShow extends AsyncTask<String, Void, Boolean> {
String type;
String password;
String name;
String address;
String seed;
String viewKey;
boolean isWatchOnly;
@Override
public void run() {
final Wallet wallet = WalletManager.getInstance().openWallet(walletPath, password);
getActivity().runOnUiThread(new Runnable() {
public void run() {
show(wallet, password, type);
wallet.close();
protected Boolean doInBackground(String... params) {
if (params.length != 3) return false;
String walletPath = params[0];
password = params[1];
type = params[2];
Wallet wallet;
boolean closeWallet;
if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) {
wallet = GenerateReviewFragment.this.walletCallback.getWallet();
closeWallet = false;
} else {
wallet = WalletManager.getInstance().openWallet(walletPath, password);
closeWallet = true;
}
});
}
}
, "DetailsReview", MoneroHandlerThread.THREAD_STACK_SIZE).start();
if (wallet.getStatus() != Wallet.Status.Status_Ok) return false;
name = wallet.getName();
address = wallet.getAddress();
seed = wallet.getSeed();
viewKey = wallet.getSecretViewKey();
isWatchOnly = wallet.isWatchOnly();
if (closeWallet) wallet.close();
return true;
}
private void show(final Wallet wallet, final String password, final String type) {
if (type.equals(GenerateReviewFragment.VIEW_ACCEPT)) {
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
if (type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT)) {
tvWalletPassword.setText(password);
bAccept.setVisibility(View.VISIBLE);
bAccept.setEnabled(true);
}
tvWalletName.setText(wallet.getName());
tvWalletAddress.setText(wallet.getAddress());
tvWalletMnemonic.setText(wallet.getSeed());
tvWalletViewKey.setText(wallet.getSecretViewKey());
String spend = wallet.isWatchOnly() ? "" : "not available - use seed for recovery";
tvWalletName.setText(name);
tvWalletAddress.setText(address);
tvWalletMnemonic.setText(seed);
tvWalletViewKey.setText(viewKey);
String spend = isWatchOnly ? "" : "not available - use seed for recovery";
if (spend.length() > 0) { //TODO should be == 64, but spendkey is not in the API yet
tvWalletSpendKey.setText(spend);
} else {
tvWalletSpendKey.setText(getString(R.string.generate_wallet_watchonly));
}
}
hideProgress();
}
}
GenerateReviewFragment.Listener acceptCallback = null;
GenerateReviewFragment.ListenerWithWallet walletCallback = null;
@ -141,6 +161,7 @@ public class GenerateReviewFragment extends Fragment {
public interface ListenerWithWallet {
Wallet getWallet();
}
@Override

View File

@ -16,12 +16,15 @@
package com.m2049r.xmrwallet;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
@ -77,16 +80,22 @@ public class LoginActivity extends AppCompatActivity
}
}
@Override
public void onWalletSelected(final String walletName) {
boolean checkServiceRunning() {
if (WalletService.Running) {
Toast.makeText(this, getString(R.string.service_busy), Toast.LENGTH_SHORT).show();
return;
return true;
} else {
return false;
}
}
@Override
public void onWalletSelected(final String walletName) {
if (checkServiceRunning()) return;
Log.d(TAG, "selected wallet is ." + walletName + ".");
// now it's getting real, check if wallet exists
String walletPath = Helper.getWalletPath(this, walletName);
if (WalletManager.getInstance().walletExists(walletPath)) {
File walletFile = Helper.getWalletFile(this, walletName);
if (WalletManager.getInstance().walletExists(walletFile)) {
promptPassword(walletName, new PasswordAction() {
@Override
public void action(String walletName, String password) {
@ -101,12 +110,50 @@ public class LoginActivity extends AppCompatActivity
@Override
public void onWalletDetails(final String walletName) {
Log.d(TAG, "details for wallet ." + walletName + ".");
final String walletPath = Helper.getWalletPath(this, walletName);
if (WalletManager.getInstance().walletExists(walletPath)) {
if (checkServiceRunning()) return;
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
final File walletFile = Helper.getWalletFile(LoginActivity.this, walletName);
if (WalletManager.getInstance().walletExists(walletFile)) {
promptPassword(walletName, new PasswordAction() {
@Override
public void action(String walletName, String password) {
startDetails(walletPath, password, GenerateReviewFragment.VIEW_DETAILS);
startDetails(walletFile, password, GenerateReviewFragment.VIEW_TYPE_DETAILS);
}
});
} else { // this cannot really happen as we prefilter choices
Log.e(TAG, "Wallet missing: " + 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 AlertDialog.Builder(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();
}
@Override
public void onWalletReceive(String walletName) {
Log.d(TAG, "receive for wallet ." + walletName + ".");
if (checkServiceRunning()) return;
final File walletFile = Helper.getWalletFile(this, walletName);
if (WalletManager.getInstance().walletExists(walletFile)) {
promptPassword(walletName, new PasswordAction() {
@Override
public void action(String walletName, String password) {
startReceive(walletFile, password);
}
});
} else { // this cannot really happen as we prefilter choices
@ -114,8 +161,224 @@ public class LoginActivity extends AppCompatActivity
}
}
private class AsyncRename extends AsyncTask<String, Void, Boolean> {
ProgressDialog progressDialog = new MyProgressDialog(LoginActivity.this, R.string.rename_progress);
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog.show();
LoginActivity.this.asyncWaitTask = this;
}
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 2) return false;
File walletFile = Helper.getWalletFile(LoginActivity.this, params[0]);
String newName = params[1];
return renameWallet(walletFile, newName);
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
progressDialog.dismiss();
if (result) {
reloadWalletList();
} else {
Toast.makeText(LoginActivity.this, getString(R.string.rename_failed), Toast.LENGTH_LONG).show();
}
LoginActivity.this.asyncWaitTask = null;
}
}
// copy + delete seems safer than rename bevause we call rollback easily
boolean renameWallet(File walletFile, String newName) {
if (copyWallet(walletFile, new File(walletFile.getParentFile(), newName), false)) {
deleteWallet(walletFile);
return true;
} else {
return false;
}
}
@Override
public void onWalletRename(final String walletName) {
Log.d(TAG, "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 AlertDialog.Builder(this);
alertDialogBuilder.setView(promptsView);
final EditText etRename = (EditText) promptsView.findViewById(R.id.etRename);
final TextView tvRenameLabel = (TextView) promptsView.findViewById(R.id.tvRenameLabel);
tvRenameLabel.setText(getString(R.string.prompt_rename) + " " + walletName);
// set dialog message
alertDialogBuilder
.setCancelable(false)
.setPositiveButton("OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Helper.hideKeyboardAlways(LoginActivity.this);
String newName = etRename.getText().toString();
new AsyncRename().execute(walletName, newName);
}
})
.setNegativeButton("Cancel",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Helper.hideKeyboardAlways(LoginActivity.this);
dialog.cancel();
}
});
final AlertDialog dialog = alertDialogBuilder.create();
Helper.showKeyboard(dialog);
// accept keyboard "ok"
etRename.setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
Helper.hideKeyboardAlways(LoginActivity.this);
String newName = etRename.getText().toString();
dialog.cancel();
new AsyncRename().execute(walletName, newName);
return false;
}
return false;
}
});
dialog.show();
}
private class AsyncBackup extends AsyncTask<String, Void, Boolean> {
ProgressDialog progressDialog = new MyProgressDialog(LoginActivity.this, R.string.backup_progress);
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog.show();
LoginActivity.this.asyncWaitTask = this;
}
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 1) return false;
return backupWallet(params[0]);
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
progressDialog.dismiss();
if (!result) {
Toast.makeText(LoginActivity.this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show();
}
LoginActivity.this.asyncWaitTask = null;
}
}
private boolean backupWallet(String walletName) {
File backupFolder = new File(getStorageRoot(), ".backups");
if (!backupFolder.exists()) {
if (!backupFolder.mkdir()) {
Log.e(TAG, "Cannot create backup dir " + backupFolder.getAbsolutePath());
return false;
}
}
File walletFile = Helper.getWalletFile(LoginActivity.this, walletName);
File backupFile = new File(backupFolder, walletName);
Log.d(TAG, "backup " + walletFile.getAbsolutePath() + " to " + backupFile.getAbsolutePath());
// TODO probably better to copy to a new file and then rename
// then if something fails we have the old backup at least
// or just create a new backup every time and keep n old backups
return copyWallet(walletFile, backupFile, true);
}
@Override
public void onWalletBackup(String walletName) {
Log.d(TAG, "backup for wallet ." + walletName + ".");
new AsyncBackup().execute(walletName);
}
private class AsyncArchive extends AsyncTask<String, Void, Boolean> {
ProgressDialog progressDialog = new MyProgressDialog(LoginActivity.this, R.string.archive_progress);
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog.show();
LoginActivity.this.asyncWaitTask = this;
}
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 1) return false;
String walletName = params[0];
if (backupWallet(walletName) && deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) {
return true;
} else {
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
progressDialog.dismiss();
if (result) {
reloadWalletList();
} else {
Toast.makeText(LoginActivity.this, getString(R.string.archive_failed), Toast.LENGTH_LONG).show();
}
LoginActivity.this.asyncWaitTask = null;
}
}
@Override
public void onWalletArchive(final String walletName) {
Log.d(TAG, "archive for wallet ." + walletName + ".");
if (checkServiceRunning()) return;
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
new AsyncArchive().execute(walletName);
break;
case DialogInterface.BUTTON_NEGATIVE:
// do nothing
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getString(R.string.archive_alert_message))
.setTitle(walletName)
.setPositiveButton(getString(R.string.archive_alert_yes), dialogClickListener)
.setNegativeButton(getString(R.string.archive_alert_no), dialogClickListener)
.show();
}
void reloadWalletList() {
try {
LoginFragment loginFragment = (LoginFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
loginFragment.loadList();
} catch (ClassCastException ex) {
}
}
@Override
public void onAddWallet() {
if (checkServiceRunning()) return;
startGenerateFragment();
}
@ -236,10 +499,64 @@ public class LoginActivity extends AppCompatActivity
super.onPause();
}
AsyncTask asyncWaitTask = null;
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume()");
if (WalletService.Running && (asyncWaitTask == null)) {
Log.d(TAG, "new process dialog");
new AsyncWaitForService().execute();
}
}
private class MyProgressDialog extends ProgressDialog {
Activity activity;
public MyProgressDialog(Activity activity, int msgId) {
super(activity);
this.activity = activity;
setCancelable(false);
setMessage(activity.getString(msgId));
}
@Override
public void onBackPressed() {
//activity.finish();
}
}
private class AsyncWaitForService extends AsyncTask<Void, Void, Void> {
ProgressDialog progressDialog = new MyProgressDialog(LoginActivity.this, R.string.service_progress);
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog.show();
LoginActivity.this.asyncWaitTask = this;
}
@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);
progressDialog.dismiss();
LoginActivity.this.asyncWaitTask = null;
}
}
@ -251,15 +568,23 @@ public class LoginActivity extends AppCompatActivity
startActivity(intent);
}
void startDetails(final String walletPath, final String password, String type) {
void startDetails(File walletFile, String password, String type) {
Log.d(TAG, "startDetails()");
Bundle b = new Bundle();
b.putString("name", walletPath);
b.putString("path", walletFile.getAbsolutePath());
b.putString("password", password);
b.putString("type", type);
startReviewFragment(b);
}
void startReceive(File walletFile, String password) {
Log.d(TAG, "startReceive()");
Bundle b = new Bundle();
b.putString("path", walletFile.getAbsolutePath());
b.putString("password", password);
startReceiveFragment(b);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
Log.d(TAG, "onRequestPermissionsResult()");
@ -268,7 +593,7 @@ public class LoginActivity extends AppCompatActivity
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startLoginFragment();
startLoginFragment = true;
} else {
String msg = getString(R.string.message_strorage_not_permitted);
Log.e(TAG, msg);
@ -280,6 +605,17 @@ public class LoginActivity extends AppCompatActivity
}
}
private boolean startLoginFragment = false;
@Override
protected void onResumeFragments() {
super.onResumeFragments();
if (startLoginFragment) {
startLoginFragment();
startLoginFragment = false;
}
}
void startLoginFragment() {
Fragment fragment = new LoginFragment();
getSupportFragmentManager().beginTransaction()
@ -297,6 +633,11 @@ public class LoginActivity extends AppCompatActivity
Log.d(TAG, "GenerateReviewFragment placed");
}
void startReceiveFragment(Bundle extras) {
replaceFragment(new ReceiveFragment(), null, extras);
Log.d(TAG, "ReceiveFragment placed");
}
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
if (extras != null) {
newFragment.setArguments(extras);
@ -316,6 +657,7 @@ public class LoginActivity extends AppCompatActivity
//////////////////////////////////////////
static final String MNEMONIC_LANGUAGE = "English"; // see mnemonics/electrum-words.cpp for more
// TODO make this an AsyncTask?
public void createWallet(final String name, final String password, final WalletCreator walletCreator) {
final GenerateFragment genFragment = (GenerateFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
@ -336,7 +678,7 @@ public class LoginActivity extends AppCompatActivity
cacheFile.delete();
File keysFile = new File(newWalletFolder, name + ".keys");
keysFile.delete();
final File addressFile = new File(newWalletFolder, name + ".address.txt");
File addressFile = new File(newWalletFolder, name + ".address.txt");
addressFile.delete();
if (cacheFile.exists() || keysFile.exists() || addressFile.exists()) {
@ -345,29 +687,29 @@ public class LoginActivity extends AppCompatActivity
return;
}
String newWalletPath = new File(newWalletFolder, name).getAbsolutePath();
boolean success = walletCreator.createWallet(newWalletPath, password);
File newWalletFile = new File(newWalletFolder, name);
boolean success = walletCreator.createWallet(newWalletFile, password);
if (success) {
startDetails(newWalletPath, password, GenerateReviewFragment.VIEW_ACCEPT);
startDetails(newWalletFile, password, GenerateReviewFragment.VIEW_TYPE_ACCEPT);
} else {
Toast.makeText(LoginActivity.this,
getString(R.string.generate_wallet_create_failed), Toast.LENGTH_LONG).show();
Log.e(TAG, "Could not create new wallet in " + newWalletPath);
Log.e(TAG, "Could not create new wallet in " + newWalletFile.getAbsolutePath());
genFragment.walletGenerateError();
}
}
interface WalletCreator {
boolean createWallet(String path, String password);
boolean createWallet(File aFile, String password);
}
@Override
public void onGenerate(String name, String password) {
createWallet(name, password,
new WalletCreator() {
public boolean createWallet(String path, String password) {
public boolean createWallet(File aFile, String password) {
Wallet newWallet = WalletManager.getInstance()
.createWallet(path, password, MNEMONIC_LANGUAGE);
.createWallet(aFile, password, MNEMONIC_LANGUAGE);
boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok);
if (!success) Log.e(TAG, newWallet.getErrorString());
newWallet.close();
@ -380,12 +722,15 @@ public class LoginActivity extends AppCompatActivity
public void onGenerate(String name, String password, final String seed, final long restoreHeight) {
createWallet(name, password,
new WalletCreator() {
public boolean createWallet(String path, String password) {
Wallet newWallet = WalletManager.getInstance().recoveryWallet(path, seed, restoreHeight);
public boolean createWallet(File aFile, String password) {
Wallet newWallet = WalletManager.getInstance().recoveryWallet(aFile, seed, restoreHeight);
boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok);
if (!success) Log.e(TAG, newWallet.getErrorString());
if (success) {
newWallet.setPassword(password);
success = success && newWallet.store();
} else {
Log.e(TAG, newWallet.getErrorString());
}
newWallet.close();
return success;
}
@ -397,14 +742,17 @@ public class LoginActivity extends AppCompatActivity
final String address, final String viewKey, final String spendKey, final long restoreHeight) {
createWallet(name, password,
new WalletCreator() {
public boolean createWallet(String path, String password) {
public boolean createWallet(File aFile, String password) {
Wallet newWallet = WalletManager.getInstance()
.createWalletFromKeys(path, MNEMONIC_LANGUAGE, restoreHeight,
.createWalletFromKeys(aFile, MNEMONIC_LANGUAGE, restoreHeight,
address, viewKey, spendKey);
boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok);
if (!success) Log.e(TAG, newWallet.getErrorString());
if (success) {
newWallet.setPassword(password);
success = success && newWallet.store();
} else {
Log.e(TAG, newWallet.getErrorString());
}
newWallet.close();
return success;
}
@ -414,18 +762,18 @@ public class LoginActivity extends AppCompatActivity
@Override
public void onAccept(final String name, final String password) {
final File newWalletFolder = new File(getStorageRoot(), ".new");
final File newWalletFile = new File(new File(getStorageRoot(), ".new"), name);
final File walletFolder = getStorageRoot();
final String walletPath = new File(walletFolder, name).getAbsolutePath();
final boolean rc = copyWallet(walletFolder, newWalletFolder, name)
final File walletFile = new File(walletFolder, name);
final boolean rc = copyWallet(newWalletFile, walletFile, false)
&&
(testWallet(walletPath, password) == Wallet.Status.Status_Ok);
(testWallet(walletFile.getAbsolutePath(), password) == Wallet.Status.Status_Ok);
if (rc) {
popFragmentStack(GENERATE_STACK);
Toast.makeText(LoginActivity.this,
getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "Wallet store failed to " + walletPath);
Log.e(TAG, "Wallet store failed to " + walletFile.getAbsolutePath());
Toast.makeText(LoginActivity.this,
getString(R.string.generate_wallet_create_failed_2), Toast.LENGTH_LONG).show();
}
@ -441,30 +789,58 @@ public class LoginActivity extends AppCompatActivity
return status;
}
boolean copyWallet(File dstDir, File srcDir, String name) {
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) {
//Log.d(TAG, "src=" + srcWallet.exists() + " dst=" + dstWallet.exists());
if (walletExists(dstWallet, true) && !overwrite) return false;
if (!walletExists(srcWallet, false)) return false;
boolean success = false;
File srcDir = srcWallet.getParentFile();
String srcName = srcWallet.getName();
File dstDir = dstWallet.getParentFile();
String dstName = dstWallet.getName();
try {
// the cache is corrupt if we recover (!!)
// the cache is ok if we immediately do a full refresh()
// recoveryheight is ignored but not on watchonly wallet ?! - find out why
// so we just ignore the cache file and rebuild it on first sync
//copyFile(dstDir, srcDir, name);
copyFile(dstDir, srcDir, name + ".keys");
copyFile(dstDir, srcDir, name + ".address.txt");
copyFile(new File(srcDir, srcName), new File(dstDir, dstName));
copyFile(new File(srcDir, srcName + ".keys"), new File(dstDir, dstName + ".keys"));
copyFile(new File(srcDir, srcName + ".address.txt"), new File(dstDir, dstName + ".address.txt"));
success = true;
} catch (IOException ex) {
Log.e(TAG, "wallet copy failed: " + ex.getMessage());
// try to rollback
new File(dstDir, name).delete();
new File(dstDir, name + ".keys").delete();
new File(dstDir, name + ".address.txt").delete();
deleteWallet(dstWallet);
}
return success;
}
void copyFile(File dstDir, File srcDir, String name) throws IOException {
FileChannel inChannel = new FileInputStream(new File(srcDir, name)).getChannel();
FileChannel outChannel = new FileOutputStream(new File(dstDir, name)).getChannel();
// do our best to delete as much as possible of the wallet files
boolean deleteWallet(File walletFile) {
Log.d(TAG, "deleteWallet " + walletFile.getAbsolutePath());
if (!walletFile.isFile()) return false;
File dir = walletFile.getParentFile();
String name = walletFile.getName();
boolean success = new File(dir, name).delete();
success = new File(dir, name + ".keys").delete() && success;
success = new File(dir, name + ".address.txt").delete() && success;
return success;
}
void copyFile(File src, File dst) throws IOException {
FileChannel inChannel = new FileInputStream(src).getChannel();
FileChannel outChannel = new FileOutputStream(dst).getChannel();
try {
inChannel.transferTo(0, inChannel.size(), outChannel);
} finally {

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.util.Log;
@ -82,9 +83,17 @@ public class LoginFragment extends Fragment {
File getStorageRoot();
void onWalletSelected(final String wallet);
void onWalletSelected(String wallet);
void onWalletDetails(final String wallet);
void onWalletDetails(String wallet);
void onWalletReceive(String wallet);
void onWalletRename(String name);
void onWalletBackup(String name);
void onWalletArchive(String walletName);
void onAddWallet();
@ -117,14 +126,14 @@ public class LoginFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onPause()");
Log.d(TAG, "onResume()");
activityCallback.setTitle(getString(R.string.login_activity_name));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "onCreateView");
View view = inflater.inflate(R.layout.login_fragment, container, false);
tbMainNet = (ToggleButton) view.findViewById(R.id.tbMainNet);
@ -222,31 +231,6 @@ public class LoginFragment extends Fragment {
}
});
/* listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
// Difference to opening wallet is that we don't need a daemon set
String itemValue = (String) listView.getItemAtPosition(position);
if (itemValue.length() <= (WALLETNAME_PREAMBLE_LENGTH)) {
Toast.makeText(getActivity(), getString(R.string.panic), Toast.LENGTH_LONG).show();
return true;
}
String wallet = itemValue.substring(WALLETNAME_PREAMBLE_LENGTH);
String x = isMainNet() ? "4" : "9A";
if (x.indexOf(itemValue.charAt(1)) < 0) {
Toast.makeText(getActivity(), getString(R.string.prompt_wrong_net), Toast.LENGTH_LONG).show();
return true;
}
checkAndSetWalletDaemon("", !isMainNet()); // just set selected net
activityCallback.onWalletDetails(wallet);
return true;
}
});
*/
loadList();
return view;
}
@ -260,7 +244,8 @@ public class LoginFragment extends Fragment {
}
}
private void loadList() {
public void loadList() {
Log.d(TAG, "loadList()");
WalletManager mgr = WalletManager.getInstance();
List<WalletManager.WalletInfo> walletInfos =
mgr.findWallets(activityCallback.getStorageRoot());
@ -408,32 +393,52 @@ public class LoginFragment extends Fragment {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
String listItem = (String) listView.getItemAtPosition(info.position);
String name = nameFromListItem(listItem, !isMainNet());
if (name == null) {
Toast.makeText(getActivity(), getString(R.string.panic), Toast.LENGTH_LONG).show();
}
switch (item.getItemId()) {
case R.id.action_info:
String listItem = (String) listView.getItemAtPosition(info.position);
return showInfo(listItem);
showInfo(name);
break;
case R.id.action_receive:
showReceive(name);
break;
case R.id.action_rename:
activityCallback.onWalletRename(name);
break;
case R.id.action_backup:
activityCallback.onWalletBackup(name);
break;
case R.id.action_archive:
activityCallback.onWalletArchive(name);
break;
default:
return super.onContextItemSelected(item);
}
}
private boolean showInfo(String listItem) {
if (listItem.length() <= (WALLETNAME_PREAMBLE_LENGTH)) {
Toast.makeText(getActivity(), getString(R.string.panic), Toast.LENGTH_LONG).show();
return true;
}
String wallet = listItem.substring(WALLETNAME_PREAMBLE_LENGTH);
String x = isMainNet() ? "4" : "9A";
if (x.indexOf(listItem.charAt(1)) < 0) {
Toast.makeText(getActivity(), getString(R.string.prompt_wrong_net), Toast.LENGTH_LONG).show();
return true;
}
private void showInfo(@NonNull String name) {
checkAndSetWalletDaemon("", !isMainNet()); // just set selected net
activityCallback.onWalletDetails(wallet);
activityCallback.onWalletDetails(name);
}
private boolean showReceive(@NonNull String name) {
checkAndSetWalletDaemon("", !isMainNet()); // just set selected net
activityCallback.onWalletReceive(name);
return true;
}
private String nameFromListItem(String listItem, boolean testnet) {
String wallet = listItem.substring(WALLETNAME_PREAMBLE_LENGTH);
String x = testnet ? "9A" : "4";
if (x.indexOf(listItem.charAt(1)) < 0) {
return null;
}
return wallet;
}
}

View File

@ -0,0 +1,349 @@
/*
* 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.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
import java.util.HashMap;
import java.util.Map;
public class ReceiveFragment extends Fragment {
static final String TAG = "ReceiveFragment";
ProgressBar pbProgress;
TextView tvAddress;
EditText etPaymentId;
EditText etAmount;
Button bPaymentId;
Button bGenerate;
ImageView qrCode;
EditText etDummy;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.receive_fragment, container, false);
pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress);
tvAddress = (TextView) view.findViewById(R.id.tvAddress);
etPaymentId = (EditText) view.findViewById(R.id.etPaymentId);
etAmount = (EditText) view.findViewById(R.id.etAmount);
bPaymentId = (Button) view.findViewById(R.id.bPaymentId);
qrCode = (ImageView) view.findViewById(R.id.qrCode);
bGenerate = (Button) view.findViewById(R.id.bGenerate);
etDummy = (EditText) view.findViewById(R.id.etDummy);
etPaymentId.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etPaymentId.setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_NEXT)) {
if (paymentIdOk()) {
etAmount.requestFocus();
} // otherwise ignore
return true;
}
return false;
}
});
etPaymentId.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
qrCode.setImageBitmap(getMoneroLogo());
if (paymentIdOk() && amountOk()) {
bGenerate.setEnabled(true);
} else {
bGenerate.setEnabled(false);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
etAmount.setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
if (paymentIdOk() && amountOk()) {
Helper.hideKeyboard(getActivity());
generateQr();
}
return true;
}
return false;
}
});
etAmount.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
qrCode.setImageBitmap(getMoneroLogo());
if (paymentIdOk() && amountOk()) {
bGenerate.setEnabled(true);
} else {
bGenerate.setEnabled(false);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
bPaymentId.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
etPaymentId.setText((Wallet.generatePaymentId()));
etPaymentId.setSelection(etPaymentId.getText().length());
if (paymentIdOk() && amountOk()) {
generateQr();
}
}
});
bGenerate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (paymentIdOk() && amountOk()) {
Helper.hideKeyboard(getActivity());
generateQr();
}
}
});
showProgress();
qrCode.setImageBitmap(getMoneroLogo());
Bundle b = getArguments();
String address = b.getString("address");
if (address == null) {
String path = b.getString("path");
String password = b.getString("password");
show(path, password);
} else {
show(address);
}
return view;
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, "onResume()");
if (paymentIdOk() && amountOk() && tvAddress.getText().length() > 0) {
generateQr();
}
}
private void show(String address) {
tvAddress.setText(address);
etPaymentId.setEnabled(true);
etAmount.setEnabled(true);
bPaymentId.setEnabled(true);
bGenerate.setEnabled(true);
hideProgress();
generateQr();
}
private void show(String walletPath, String password) {
new ReceiveFragment.AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR,
walletPath, password);
}
private class AsyncShow extends AsyncTask<String, Void, Boolean> {
String password;
String address;
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 2) return false;
String walletPath = params[0];
password = params[1];
Wallet wallet = WalletManager.getInstance().openWallet(walletPath, password);
address = wallet.getAddress();
wallet.close();
return true;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (result) {
show(address);
} else {
Toast.makeText(getActivity(), getString(R.string.receive_cannot_open), Toast.LENGTH_LONG).show();
hideProgress();
}
}
}
private boolean amountOk() {
String amountEntry = etAmount.getText().toString();
if (amountEntry.isEmpty()) return true;
long amount = Wallet.getAmountFromString(amountEntry);
return (amount > 0);
}
private boolean paymentIdOk() {
String paymentId = etPaymentId.getText().toString();
return paymentId.isEmpty() || Wallet.isPaymentIdValid(paymentId);
}
private void generateQr() {
String address = tvAddress.getText().toString();
String paymentId = etPaymentId.getText().toString();
String enteredAmount = etAmount.getText().toString();
// that's a lot of converting ...
String amount = (enteredAmount.isEmpty() ? enteredAmount : Helper.getDisplayAmount(Wallet.getAmountFromString(enteredAmount)));
StringBuffer sb = new StringBuffer();
sb.append(ScannerFragment.QR_SCHEME).append(address);
boolean first = true;
if (!paymentId.isEmpty()) {
if (first) {
sb.append("?");
first = false;
}
sb.append(ScannerFragment.QR_PAYMENTID).append('=').append(paymentId);
}
if (!amount.isEmpty()) {
if (first) {
sb.append("?");
} else {
sb.append("&");
}
sb.append(ScannerFragment.QR_AMOUNT).append('=').append(amount);
}
String text = sb.toString();
Bitmap qr = generate(text, 500, 500);
if (qr != null) {
etAmount.setText(amount);
qrCode.setImageBitmap(qr);
etDummy.requestFocus();
bGenerate.setEnabled(false);
}
}
public Bitmap generate(String text, int width, int height) {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
try {
BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
int[] pixels = new int[width * height];
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
if (bitMatrix.get(j, i)) {
pixels[i * width + j] = 0x00000000;
} else {
pixels[i * height + j] = 0xffffffff;
}
}
}
Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.RGB_565);
bitmap = addLogo(bitmap);
return bitmap;
} catch (WriterException e) {
e.printStackTrace();
}
return null;
}
// TODO check if we can sensibly cache some of this
private Bitmap addLogo(Bitmap qrBitmap) {
Bitmap logo = getMoneroLogo();
int qrWidth = qrBitmap.getWidth();
int qrHeight = qrBitmap.getHeight();
int logoWidth = logo.getWidth();
int logoHeight = logo.getHeight();
Bitmap logoBitmap = Bitmap.createBitmap(qrWidth, qrHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(logoBitmap);
canvas.drawBitmap(qrBitmap, 0, 0, null);
canvas.save(Canvas.ALL_SAVE_FLAG);
// 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 / 2, qrHeight / 2);
canvas.drawBitmap(logo, (qrWidth - logoWidth) / 2, (qrHeight - logoHeight) / 2, null);
canvas.restore();
return logoBitmap;
}
private Bitmap logo = null;
private Bitmap getMoneroLogo() {
if (logo == null) {
logo = Helper.getBitmap(getContext(), R.drawable.ic_monero_qr);
}
return logo;
}
public void showProgress() {
pbProgress.setIndeterminate(true);
pbProgress.setVisibility(View.VISIBLE);
}
public void hideProgress() {
pbProgress.setVisibility(View.GONE);
}
}

View File

@ -100,8 +100,8 @@ public class SendFragment extends Fragment {
pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress);
etAddress.setRawInputType(InputType.TYPE_CLASS_TEXT);
etPaymentId.setRawInputType(InputType.TYPE_CLASS_TEXT);
etAddress.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etPaymentId.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
etNotes.setRawInputType(InputType.TYPE_CLASS_TEXT);
Helper.showKeyboard(getActivity());
@ -209,7 +209,8 @@ public class SendFragment extends Fragment {
bPaymentId.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
etPaymentId.setText((activityCallback.generatePaymentId()));
etPaymentId.setText((Wallet.generatePaymentId()));
etPaymentId.setSelection(etPaymentId.getText().length());
}
});
@ -285,7 +286,7 @@ public class SendFragment extends Fragment {
private boolean paymentIdOk() {
String paymentId = etPaymentId.getText().toString();
return paymentId.isEmpty() || activityCallback.isPaymentIdValid(paymentId);
return paymentId.isEmpty() || Wallet.isPaymentIdValid(paymentId);
}
private void prepareSend() {
@ -358,10 +359,6 @@ public class SendFragment extends Fragment {
void onSend(String notes);
String generatePaymentId();
boolean isPaymentIdValid(String paymentId);
String getWalletAddress();
void onDisposeRequest();

View File

@ -16,13 +16,14 @@
package com.m2049r.xmrwallet;
import android.app.AlertDialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.net.UrlQuerySanitizer;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
@ -33,8 +34,6 @@ import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
@ -90,20 +89,21 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
}
private void startWalletService() {
acquireWakeLock();
Bundle extras = getIntent().getExtras();
if (extras != null) {
acquireWakeLock();
String walletId = extras.getString(REQUEST_ID);
String walletPassword = extras.getString(REQUEST_PW);
connectWalletService(walletId, walletPassword);
} else {
throw new IllegalStateException("No extras passed! Panic!");
finish();
//throw new IllegalStateException("No extras passed! Panic!");
}
}
private void stopWalletService() {
releaseWakeLock();
disconnectWalletService();
releaseWakeLock();
}
@Override
@ -120,11 +120,8 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (!haveWallet) return true;
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.wallet_menu, menu);
return true;
public boolean hasWallet() {
return haveWallet;
}
@Override
@ -132,13 +129,15 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
switch (item.getItemId()) {
case R.id.action_info:
onWalletDetails();
break;
return true;
case R.id.action_receive:
onWalletReceive();
return true;
default:
break;
return super.onOptionsItemSelected(item);
}
}
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -561,16 +560,6 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
}
}
@Override
public String generatePaymentId() {
return getWallet().generatePaymentId();
}
@Override
public boolean isPaymentIdValid(String paymentId) {
return Wallet.isPaymentIdValid(paymentId);
}
@Override
public String getWalletAddress() {
return getWallet().getAddress();
@ -595,27 +584,49 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
}
private void onWalletDetails() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (!(fragment instanceof GenerateReviewFragment)) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
Bundle extras = new Bundle();
extras.putString("type", GenerateReviewFragment.VIEW_WALLET);
extras.putString("type", GenerateReviewFragment.VIEW_TYPE_WALLET);
replaceFragment(new GenerateReviewFragment(), null, extras);
break;
case DialogInterface.BUTTON_NEGATIVE:
// do nothing
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(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();
}
@Override
public void onDisposeRequest() {
getWallet().disposePendingTransaction();
}
private boolean startScanFragment = false;
@Override
protected void onResumeFragments() {
super.onResumeFragments();
if (startScanFragment) {
startScanFragment();
startScanFragment = false;
}
}
private void startScanFragment() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (fragment instanceof SendFragment) {
Bundle extras = new Bundle();
replaceFragment(new ScannerFragment(), null, extras);
}
}
/// QR scanner callbacks
@Override
@ -702,7 +713,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startScanFragment();
startScanFragment = true;
} else {
String msg = getString(R.string.message_camera_not_permitted);
Log.e(TAG, msg);
@ -713,4 +724,21 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment.
}
}
@Override
public void onWalletReceive() {
startReceive(getWalletAddress());
}
void startReceive(String address) {
Log.d(TAG, "startReceive()");
Bundle b = new Bundle();
b.putString("address", address);
startReceiveFragment(b);
}
void startReceiveFragment(Bundle extras) {
replaceFragment(new ReceiveFragment(), null, extras);
Log.d(TAG, "ReceiveFragment placed");
}
}

View File

@ -18,12 +18,16 @@ package com.m2049r.xmrwallet;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.constraint.ConstraintLayout;
import android.support.v4.app.Fragment;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@ -53,6 +57,19 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O
ProgressBar pbProgress;
Button bSend;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (activityCallback.hasWallet())
inflater.inflate(R.menu.wallet_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -236,6 +253,10 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O
boolean isWatchOnly();
String getTxKey(String txId);
void onWalletReceive();
boolean hasWallet();
}
@Override

View File

@ -238,6 +238,7 @@ public class Wallet {
public native void setDefaultMixin(int mixin);
public native boolean setUserNote(String txid, String note);
public native String getUserNote(String txid);
public native String getTxKey(String txid);

View File

@ -71,8 +71,8 @@ public class WalletManager {
managedWallets.remove(walletId);
}
public Wallet createWallet(String path, String password, String language) {
long walletHandle = createWalletJ(path, password, language, isTestNet());
public Wallet createWallet(File aFile, String password, String language) {
long walletHandle = createWalletJ(aFile.getAbsolutePath(), password, language, isTestNet());
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet.getName(), wallet);
return wallet;
@ -89,14 +89,14 @@ public class WalletManager {
private native long openWalletJ(String path, String password, boolean isTestNet);
public Wallet recoveryWallet(String path, String mnemonic) {
Wallet wallet = recoveryWallet(path, mnemonic, 0);
public Wallet recoveryWallet(File aFile, String mnemonic) {
Wallet wallet = recoveryWallet(aFile, mnemonic, 0);
manageWallet(wallet.getName(), wallet);
return wallet;
}
public Wallet recoveryWallet(String path, String mnemonic, long restoreHeight) {
long walletHandle = recoveryWalletJ(path, mnemonic, isTestNet(), restoreHeight);
public Wallet recoveryWallet(File aFile, String mnemonic, long restoreHeight) {
long walletHandle = recoveryWalletJ(aFile.getAbsolutePath(), mnemonic, isTestNet(), restoreHeight);
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet.getName(), wallet);
return wallet;
@ -104,9 +104,9 @@ public class WalletManager {
private native long recoveryWalletJ(String path, String mnemonic, boolean isTestNet, long restoreHeight);
public Wallet createWalletFromKeys(String path, String language, long restoreHeight,
public Wallet createWalletFromKeys(File aFile, String language, long restoreHeight,
String addressString, String viewKeyString, String spendKeyString) {
long walletHandle = createWalletFromKeysJ(path, language, isTestNet(), restoreHeight,
long walletHandle = createWalletFromKeysJ(aFile.getAbsolutePath(), language, isTestNet(), restoreHeight,
addressString, viewKeyString, spendKeyString);
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet.getName(), wallet);
@ -134,6 +134,10 @@ public class WalletManager {
return closed;
}
public boolean walletExists(File aFile) {
return walletExists(aFile.getAbsolutePath());
}
public native boolean walletExists(String path);
public native boolean verifyWalletPassword(String keys_file_name, String password, boolean watch_only);
@ -146,20 +150,11 @@ public class WalletManager {
public String address;
}
public List<WalletInfo> findWallets(File path) {
List<WalletInfo> wallets = new ArrayList<>();
Log.d(TAG, "Scanning: " + path.getAbsolutePath());
File[] found = path.listFiles(new FilenameFilter() {
public boolean accept(File dir, String filename) {
return filename.endsWith(".keys");
}
});
for (int i = 0; i < found.length; i++) {
public WalletInfo getWalletInfo(File wallet) {
WalletInfo info = new WalletInfo();
info.path = path;
String filename = found[i].getName();
info.name = filename.substring(0, filename.length() - 5); // 5 is length of ".keys"+1
File addressFile = new File(path, info.name + ".address.txt");
info.path = wallet.getParentFile();
info.name = wallet.getName();
File addressFile = new File(info.path, info.name + ".address.txt");
//Log.d(TAG, addressFile.getAbsolutePath());
info.address = "??????";
BufferedReader addressReader = null;
@ -177,7 +172,21 @@ public class WalletManager {
}
}
}
wallets.add(info);
return info;
}
public List<WalletInfo> findWallets(File path) {
List<WalletInfo> wallets = new ArrayList<>();
Log.d(TAG, "Scanning: " + path.getAbsolutePath());
File[] found = path.listFiles(new FilenameFilter() {
public boolean accept(File dir, String filename) {
return filename.endsWith(".keys");
}
});
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));
}
return wallets;
}

View File

@ -527,7 +527,7 @@ public class WalletService extends Service {
}
private Wallet openWallet(String walletName, String walletPassword) {
String path = Helper.getWalletPath(getApplicationContext(), walletName);
String path = Helper.getWalletFile(getApplicationContext(), walletName).getAbsolutePath();
showProgress(20);
Wallet wallet = null;
WalletManager walletMgr = WalletManager.getInstance();
@ -557,7 +557,7 @@ public class WalletService extends Service {
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification notification = new Notification.Builder(this)
.setContentTitle(getString(R.string.service_description))
.setSmallIcon(R.drawable.ic_notification_sync_32_32)
.setSmallIcon(R.drawable.ic_monero_32dp)
.setContentIntent(pendingIntent)
.build();
startForeground(NOTIFICATION_ID, notification);

View File

@ -21,7 +21,14 @@ import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.VectorDrawable;
import android.os.Environment;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
@ -91,12 +98,16 @@ public class Helper {
}
}
static public String getWalletPath(Context context, String aWalletName) {
// static public String getWalletPath(Context context, String aWalletName) {
// return getWalletFile(context, aWalletName).getAbsolutePath();
// }
static public File getWalletFile(Context context, String aWalletName) {
File walletDir = getStorageRoot(context);
//d(TAG, "walletdir=" + walletDir.getAbsolutePath());
File f = new File(walletDir, aWalletName);
Log.d(TAG, "wallet = " + f.getAbsolutePath() + " size=" + f.length());
return f.getAbsolutePath();
return f;
}
/* Checks if external storage is available for read and write */
@ -136,6 +147,7 @@ public class Helper {
return ((address.length() == 95) && ("4".indexOf(address.charAt(0)) >= 0));
}
}
static public String getDisplayAmount(long amount) {
String s = Wallet.getDisplayAmount(amount);
int lastZero = 0;
@ -152,4 +164,25 @@ public class Helper {
int cutoff = Math.max(lastZero, decimal + 2);
return s.substring(0, cutoff);
}
public static Bitmap getBitmap(Context context, int drawableId) {
Drawable drawable = ContextCompat.getDrawable(context, drawableId);
if (drawable instanceof BitmapDrawable) {
return BitmapFactory.decodeResource(context.getResources(), drawableId);
} else if (drawable instanceof VectorDrawable) {
return getBitmap((VectorDrawable) drawable);
} else {
throw new IllegalArgumentException("unsupported drawable type");
}
}
private static Bitmap getBitmap(VectorDrawable vectorDrawable) {
Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
vectorDrawable.draw(canvas);
return bitmap;
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.util;
import com.m2049r.xmrwallet.service.MoneroHandlerThread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class MoneroThreadPoolExecutor {
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(null, r, "MoneroTask #" + mCount.getAndIncrement(), MoneroHandlerThread.THREAD_STACK_SIZE);
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<>(128);
public static final Executor MONERO_THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
MONERO_THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
}

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportHeight="75.0"
android:viewportWidth="75.0">
<path
android:fillColor="#ff6600"
android:pathData="M 37.3,0.35329395 c -20.377,0 -36.903,16.524 -36.903,36.902 0,4.074 0.66,7.992 1.88,11.657 l 11.036,0 0,-31.049 23.987,23.987 23.987,-23.987 0,31.049 11.037,0 c 1.22,-3.665 1.88,-7.583 1.88,-11.657 0,-20.378 -16.526,-36.902 -36.904,-36.902" />
<path
android:fillColor="#4c4c4c"
android:pathData="M 21.3164,36.895994 l 0,19.537 -15.55,0 c 6.478,10.628 18.178,17.726 31.533,17.726 13.355,0 25.056,-7.098 31.533,-17.726 l -15.549,0 0,-19.537 -15.984,15.984 z" />
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportHeight="75.0"
android:viewportWidth="75.0">
<path
android:fillColor="#ffffff"
android:pathData=" M 37.3, 37.3
m -36.9, 0
a 36.9,36.9 0 1,0 73.8,0
a 36.9,36.9 0 1,0 -73.8,0" />
<path
android:fillColor="#ff6600"
android:pathData="M 37.3,0.35329395 c -20.377,0 -36.903,16.524 -36.903,36.902 0,4.074 0.66,7.992 1.88,11.657 l 11.036,0 0,-31.049 23.987,23.987 23.987,-23.987 0,31.049 11.037,0 c 1.22,-3.665 1.88,-7.583 1.88,-11.657 0,-20.378 -16.526,-36.902 -36.904,-36.902" />
<path
android:fillColor="#4c4c4c"
android:pathData="M 21.3164,36.895994 l 0,19.537 -15.55,0 c 6.478,10.628 18.178,17.726 31.533,17.726 13.355,0 25.056,-7.098 31.533,-17.726 l -15.549,0 0,-19.537 -15.984,15.984 z" />
</vector>

View File

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="230.0"
android:viewportWidth="230.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M0 0L0 230L230 230L230 0L0 0z" />
<path
android:fillColor="#FF000000"
android:pathData="M10 10L10 80L80 80L80 10L10 10M110 10L110 20L120 20L120 10L110 10M130 10L130 30L140 30L140 10L130 10M150 10L150 80L220 80L220 10L150 10z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20 20L20 70L70 70L70 20L20 20M160 20L160 70L210 70L210 20L160 20z" />
<path
android:fillColor="#FF000000"
android:pathData="M30 30L30 60L60 60L60 30L30 30M90 30L90 40L100 40L100 30L90 30M110 30L110 40L120 40L120 30L110 30M170 30L170 60L200 60L200 30L170 30M130 40L130 50L120 50L120 60L110 60L110 50L100 50L100 70L90 70L90 90L50 90L50 120L60 120L60 130L50 130L50 140L60 140L60 130L70 130L70 140L90 140L90 180L100 180L100 170L110 170L110 180L120 180L120 190L90 190L90 220L100 220L100 210L110 210L110 220L130 220L130 210L140 210L140 220L160 220L160 210L170 210L170 220L190 220L190 210L180 210L180 200L200 200L200 220L220 220L220 210L210 210L210 200L220 200L220 190L210 190L210 180L200 180L200 190L180 190L180 170L160 170L160 160L180 160L180 140L160 140L160 130L180 130L180 120L190 120L190 110L200 110L200 120L220 120L220 100L200 100L200 90L190 90L190 110L180 110L180 100L160 100L160 90L130 90L130 100L120 100L120 70L130 70L130 80L140 80L140 70L130 70L130 60L140 60L140 40L130 40z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M100 70L100 100L90 100L90 110L80 110L80 100L70 100L70 110L60 110L60 120L70 120L70 130L90 130L90 140L100 140L100 150L110 150L110 140L120 140L120 160L110 160L110 170L120 170L120 180L130 180L130 200L140 200L140 180L150 180L150 190L170 190L170 180L150 180L150 160L160 160L160 150L150 150L150 130L160 130L160 110L150 110L150 100L130 100L130 120L140 120L140 110L150 110L150 130L130 130L130 140L120 140L120 120L110 120L110 130L100 130L100 120L90 120L90 110L100 110L100 100L110 100L110 110L120 110L120 100L110 100L110 70L100 70z" />
<path
android:fillColor="#FF000000"
android:pathData="M10 90L10 120L30 120L30 130L10 130L10 140L30 140L30 130L40 130L40 120L30 120L30 110L20 110L20 100L40 100L40 90L10 90M70 110L70 120L80 120L80 110L70 110M200 130L200 140L190 140L190 150L200 150L200 170L210 170L210 160L220 160L220 150L210 150L210 130L200 130M10 150L10 220L80 220L80 150L10 150M130 150L130 160L140 160L140 150L130 150z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20 160L20 210L70 210L70 160L20 160z" />
<path
android:fillColor="#FF000000"
android:pathData="M30 170L30 200L60 200L60 170L30 170M130 170L130 180L140 180L140 170L130 170z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M200 190L200 200L210 200L210 190L200 190M110 200L110 210L120 210L120 200L110 200M150 200L150 210L160 210L160 200L150 200z" />
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -4,19 +4,13 @@
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8sp"
android:orientation="vertical">
<ProgressBar
android:id="@+id/pbProgress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8sp"
android:visibility="invisible" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
@ -143,7 +137,6 @@
android:id="@+id/tvWalletSpendKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:selectAllOnFocus="true"
android:textAlignment="center"
android:textColor="@color/colorPrimaryDark"
android:textIsSelectable="true"

View File

@ -10,8 +10,8 @@
android:id="@+id/tvPasswordLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wallet Password"
android:labelFor="@+id/etPassword"
android:text="@string/prompt_password"
android:textAppearance="?android:attr/textAppearanceLarge" />
<EditText

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp" >
<TextView
android:id="@+id/tvRenameLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/prompt_rename"
android:labelFor="@+id/etRename"
android:textAppearance="?android:attr/textAppearanceLarge" />
<EditText
android:id="@+id/etRename"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="normal"
android:inputType="text" />
</LinearLayout>

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ProgressBar
android:id="@+id/pbProgress"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8sp"
android:visibility="gone" />
<TextView
android:id="@+id/tvAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4sp"
android:layout_marginTop="4sp"
android:textIsSelectable="true"
android:selectAllOnFocus="true"
android:hint="@string/send_address_hint"
android:textAlignment="center"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4sp"
android:layout_marginTop="4sp"
android:orientation="horizontal"
android:weightSum="10">
<TextView
android:layout_width="0sp"
android:layout_height="wrap_content"
android:layout_marginRight="8sp"
android:layout_weight="3"
android:text="@string/receive_paymentid_label"
android:textAlignment="textEnd"
android:textSize="16sp" />
<EditText
android:id="@+id/etPaymentId"
android:layout_width="0sp"
android:layout_height="wrap_content"
android:layout_weight="5"
android:enabled="false"
android:hint="@string/receive_paymentid_hint"
android:imeOptions="actionNext"
android:inputType="textMultiLine"
android:textAlignment="textStart"
android:textSize="16sp" />
<Button
android:id="@+id/bPaymentId"
android:layout_width="0sp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="2"
android:background="@color/colorPrimary"
android:enabled="false"
android:minHeight="36sp"
android:text="@string/receive_paymentid_button"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4sp"
android:layout_marginTop="4sp"
android:orientation="horizontal"
android:weightSum="10">
<TextView
android:layout_width="0sp"
android:layout_height="wrap_content"
android:layout_marginRight="8sp"
android:layout_weight="3"
android:text="@string/send_amount_label"
android:textAlignment="textEnd"
android:textSize="24sp" />
<EditText
android:id="@+id/etAmount"
android:layout_width="0sp"
android:layout_height="wrap_content"
android:layout_weight="7"
android:enabled="false"
android:hint="@string/receive_amount_hint"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:textAlignment="textStart"
android:textSize="24sp" />
</LinearLayout>
<Button
android:id="@+id/bGenerate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4sp"
android:layout_marginTop="4sp"
android:background="@color/colorPrimary"
android:enabled="false"
android:minHeight="36sp"
android:text="@string/receive_generate_hint" />
<ImageView
android:id="@+id/qrCode"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_margin="16sp"
android:adjustViewBounds="true" />
<EditText
android:id="@+id/etDummy"
android:layout_width="0sp"
android:layout_height="0sp" />
</LinearLayout>

View File

@ -3,6 +3,24 @@
<item
android:id="@+id/action_info"
android:icon="@drawable/ic_info_black_24dp"
android:title="@string/menu_info" />
<item
android:id="@+id/action_receive"
android:icon="@drawable/ic_monero_qr_24dp"
android:title="@string/menu_receive" />
<item
android:id="@+id/action_rename"
android:title="@string/menu_rename" />
<item
android:id="@+id/action_backup"
android:title="@string/menu_backup" />
<item
android:id="@+id/action_archive"
android:title="@string/menu_archive" />
</menu>

View File

@ -9,4 +9,11 @@
android:title="@string/menu_info"
app:showAsAction="always" />
<item
android:id="@+id/action_receive"
android:icon="@drawable/ic_monero_qr_24dp"
android:orderInCategory="100"
android:title="@string/menu_receive"
app:showAsAction="always" />
</menu>

View File

@ -1,9 +1,26 @@
<resources>
<string name="app_name">Monerujo</string>
<string name="login_activity_name">Select Wallet</string>
<string name="login_activity_name">Monerujo</string>
<string name="wallet_activity_name">Wallet</string>
<string name="menu_info">Details</string>
<string name="menu_receive">QR Receive</string>
<string name="menu_rename">Rename</string>
<string name="menu_archive">Archive</string>
<string name="menu_backup">Backup</string>
<string name="backup_progress">Backup in progress</string>
<string name="archive_progress">Archive in progress</string>
<string name="rename_progress">Rename in progress</string>
<string name="service_progress">Wrapping things up &#8230;\nThis can take a while!</string>
<string name="backup_success">Backup successful</string>
<string name="backup_failed">Backup failed!</string>
<string name="archive_success">Archive successful</string>
<string name="archive_failed">Archive failed!</string>
<string name="delete_failed">Delete failed!</string>
<string name="rename_failed">Rename failed!</string>
<string name="prompt_daemon">[&lt;user&gt;:&lt;pass&gt;@]&lt;daemon&gt;[:&lt;port&gt;]</string>
<string name="prompt_mainnet">Net Selection</string>
@ -24,6 +41,8 @@
<string name="service_busy">I am still busy with your last wallet &#8230;</string>
<string name="prompt_rename">Rename</string>
<string name="prompt_password">Password for</string>
<string name="bad_password">Incorrect password!</string>
<string name="bad_wallet">Wallet does not exist!</string>
@ -76,6 +95,7 @@
<string name="generate_wallet_watchonly">&lt;Watch Only Wallet&gt;</string>
<string name="generate_wallet_exists">Wallet exists! Choose another name</string>
<string name="generate_wallet_dot">Wallet name may not begin with \'.\'</string>
<string name="generate_wallet_created">Wallet created</string>
<string name="generate_wallet_create_failed">Wallet create failed</string>
<string name="generate_wallet_create_failed_1">Wallet create failed (1/2)</string>
@ -151,6 +171,22 @@
<string name="tx_pending">PENDING</string>
<string name="tx_failed">FAILED</string>
<string name="receive_generate_hint">Show me the QR Code</string>
<string name="receive_paymentid_button">Generate</string>
<string name="receive_paymentid_label">PaymentID</string>
<string name="receive_paymentid_hint">(optional)</string>
<string name="receive_amount_label">Amount</string>
<string name="receive_amount_hint">XMR (optional)</string>
<string name="receive_cannot_open">Could not open wallet!</string>
<string name="details_alert_message">Sensitive data will now be shown.\nLook over your shoulder!</string>
<string name="details_alert_yes">I\'m safe</string>
<string name="details_alert_no">Take me back!</string>
<string name="archive_alert_message">The wallet will be backuped up and then deleted!</string>
<string name="archive_alert_yes">Yes, do that!</string>
<string name="archive_alert_no">No thanks!</string>
<string name="big_amount">999999.999999999999</string>
<string-array name="mixin">

View File

@ -17,11 +17,27 @@
- Only 5 decimal places shown in transactions (full amount in details - click on transaction)
- All significant figures shown in balance
- QR Code scanning - make sure to *ALWAYS* verify the scanned code is what it is advertised to be!
- QR Code for receiving
- Backup wallets to ```.backups``` folder in main wallet folder (old backups are overwritten)
- Rename wallets
- Archive (=Backup and delete)
## I cannot select and copy the mnemonic seed
Copying anything to the clipboard on Android exposes it to any other App running. So this
is a security measure to keep your seed safe(r).
## My storage is getting full
Newly generated wallets are stored in ```.new``` in the main wallet folder.
They are never erased (for now). You can delete this whole folder from time to time.
Also, the backup folder ```.backups``` is never automatically cleaned up.
You may want to do housekeeping manually with a file browser.
All wallet files (```testnet``` and ```mainnet```) are stored in the main ```Monerujo``` folder.
So be careful erasing stuff. One of the future releases will split the wallets and move ```testnet```
wallets out of there.
## I sent a transaction but it's not in my received transactions list!
Don't worry. Received transactions which are not mined yet disappear after the wallet is saved -
I blame this on the monero code. Wait for the block to be mined.

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="xmrwallet" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" version="4">
<module external.linked.project.id="xmrwallet" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>