diff --git a/.idea/.gitignore b/.idea/.gitignore index 6faf083..45c1d3b 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,2 +1,2 @@ workspace.xml -markdown-navigator* +markdown-* diff --git a/README.md b/README.md index aac7731..3a73201 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 11b160a..e4b338a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f9eb50..83f1734 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp index 088867e..edcb635 100644 --- a/app/src/main/cpp/monerujo.cpp +++ b/app/src/main/cpp/monerujo.cpp @@ -461,7 +461,6 @@ Java_com_m2049r_xmrwallet_model_WalletManager_resolveOpenAlias(JNIEnv *env, jobj //TODO static std::tuple 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) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index 53610b2..c3a7f07 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -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)); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index ece553c..0bd24e2 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -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,40 +92,64 @@ 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() { - @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(); - } - }); - } - } - , "DetailsReview", MoneroHandlerThread.THREAD_STACK_SIZE).start(); - } + private class AsyncShow extends AsyncTask { + String type; + String password; - private void show(final Wallet wallet, final String password, final String type) { - if (type.equals(GenerateReviewFragment.VIEW_ACCEPT)) { - tvWalletPassword.setText(password); - bAccept.setVisibility(View.VISIBLE); - bAccept.setEnabled(true); + String name; + String address; + String seed; + String viewKey; + boolean isWatchOnly; + + @Override + 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; + } + 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; } - 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"; - 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)); + + @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(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(); } - hideProgress(); } GenerateReviewFragment.Listener acceptCallback = null; @@ -141,6 +161,7 @@ public class GenerateReviewFragment extends Fragment { public interface ListenerWithWallet { Wallet getWallet(); + } @Override diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index fc64961..3e8519e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -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(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) { - startDetails(walletPath, password, GenerateReviewFragment.VIEW_DETAILS); + 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 { + 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 { + 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 { + 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 { + 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()); - newWallet.setPassword(password); - success = success && newWallet.store(); + 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()); - newWallet.setPassword(password); - success = success && newWallet.store(); + 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 { diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index 4515ada..3a15f42 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -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 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; - } - - checkAndSetWalletDaemon("", !isMainNet()); // just set selected net - - activityCallback.onWalletDetails(wallet); return true; } + + private void showInfo(@NonNull String name) { + checkAndSetWalletDaemon("", !isMainNet()); // just set selected net + + 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; + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java new file mode 100644 index 0000000..efceac1 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -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 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 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); + } + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java index 72be4a4..d5617d7 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java @@ -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(); diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 9b71fae..2ebed53 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -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,14 +129,16 @@ 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) { Log.d(TAG, "onCreate()"); @@ -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,12 +584,27 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. } private void onWalletDetails() { - Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container); - if (!(fragment instanceof GenerateReviewFragment)) { - Bundle extras = new Bundle(); - extras.putString("type", GenerateReviewFragment.VIEW_WALLET); - replaceFragment(new GenerateReviewFragment(), null, extras); - } + 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_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 @@ -608,13 +612,20 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. 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); - } + Bundle extras = new Bundle(); + replaceFragment(new ScannerFragment(), null, extras); } /// QR scanner callbacks @@ -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"); + } + } \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java index ca53a84..0fcaa1f 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -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 diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java index fde4417..8496953 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/Wallet.java @@ -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); diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java index 3cf5fb5..12bb13e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java @@ -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,6 +150,31 @@ public class WalletManager { public String address; } + public WalletInfo getWalletInfo(File wallet) { + WalletInfo info = new WalletInfo(); + info.path = wallet.getParentFile(); + info.name = wallet.getName(); + File addressFile = new File(info.path, info.name + ".address.txt"); + //Log.d(TAG, addressFile.getAbsolutePath()); + info.address = "??????"; + BufferedReader addressReader = null; + try { + addressReader = new BufferedReader(new FileReader(addressFile)); + info.address = addressReader.readLine(); + } catch (IOException ex) { + Log.d(TAG, ex.getLocalizedMessage()); + } finally { + if (addressReader != null) { + try { + addressReader.close(); + } catch (IOException ex) { + // that's just too bad + } + } + } + return info; + } + public List findWallets(File path) { List wallets = new ArrayList<>(); Log.d(TAG, "Scanning: " + path.getAbsolutePath()); @@ -155,29 +184,9 @@ public class WalletManager { } }); for (int i = 0; i < found.length; i++) { - 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"); - //Log.d(TAG, addressFile.getAbsolutePath()); - info.address = "??????"; - BufferedReader addressReader = null; - try { - addressReader = new BufferedReader(new FileReader(addressFile)); - info.address = addressReader.readLine(); - } catch (IOException ex) { - Log.d(TAG, ex.getLocalizedMessage()); - } finally { - if (addressReader != null) { - try { - addressReader.close(); - } catch (IOException ex) { - // that's just too bad - } - } - } - wallets.add(info); + 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; } diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java index 5883604..7e6de93 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java +++ b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java @@ -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); diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java index f2b6b2a..e3fdee3 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -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; + } + } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/MoneroThreadPoolExecutor.java b/app/src/main/java/com/m2049r/xmrwallet/util/MoneroThreadPoolExecutor.java new file mode 100644 index 0000000..27bb64c --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/MoneroThreadPoolExecutor.java @@ -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 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; + } +} diff --git a/app/src/main/res/drawable/ic_monero_32dp.xml b/app/src/main/res/drawable/ic_monero_32dp.xml new file mode 100644 index 0000000..ea68ee8 --- /dev/null +++ b/app/src/main/res/drawable/ic_monero_32dp.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_monero_qr.xml b/app/src/main/res/drawable/ic_monero_qr.xml new file mode 100644 index 0000000..ceda495 --- /dev/null +++ b/app/src/main/res/drawable/ic_monero_qr.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_monero_qr_24dp.xml b/app/src/main/res/drawable/ic_monero_qr_24dp.xml new file mode 100644 index 0000000..54af39d --- /dev/null +++ b/app/src/main/res/drawable/ic_monero_qr_24dp.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_notification_sync_32_32.png b/app/src/main/res/drawable/ic_notification_sync_32_32.png deleted file mode 100644 index 4b88148..0000000 Binary files a/app/src/main/res/drawable/ic_notification_sync_32_32.png and /dev/null differ diff --git a/app/src/main/res/drawable/xmr_logo_256.png b/app/src/main/res/drawable/xmr_logo_256.png deleted file mode 100644 index 8abfb92..0000000 Binary files a/app/src/main/res/drawable/xmr_logo_256.png and /dev/null differ diff --git a/app/src/main/res/layout/gen_review_fragment.xml b/app/src/main/res/layout/gen_review_fragment.xml index 3650834..35fc2b8 100644 --- a/app/src/main/res/layout/gen_review_fragment.xml +++ b/app/src/main/res/layout/gen_review_fragment.xml @@ -4,19 +4,13 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - - + android:visibility="invisible" /> + android:padding="10dp"> + android:inputType="textPassword"> diff --git a/app/src/main/res/layout/prompt_rename.xml b/app/src/main/res/layout/prompt_rename.xml new file mode 100644 index 0000000..5f47af1 --- /dev/null +++ b/app/src/main/res/layout/prompt_rename.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/receive_fragment.xml b/app/src/main/res/layout/receive_fragment.xml new file mode 100644 index 0000000..46b4c3f --- /dev/null +++ b/app/src/main/res/layout/receive_fragment.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + +