diff --git a/app/src/main/cpp/monerujo.cpp b/app/src/main/cpp/monerujo.cpp index 3c11364c..79be0389 100644 --- a/app/src/main/cpp/monerujo.cpp +++ b/app/src/main/cpp/monerujo.cpp @@ -463,7 +463,7 @@ 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 Wallet +// 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 new file mode 100644 index 00000000..8d1dfd85 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -0,0 +1,179 @@ +package com.m2049r.xmrwallet; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; + +public class GenerateFragment extends Fragment { + + EditText etWalletName; + EditText etWalletPassword; + Button bGenerate; + LinearLayout llAccept; + TextView tvWalletMnemonic; + Button bAccept; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.gen_fragment, container, false); + + etWalletName = (EditText) view.findViewById(R.id.etWalletName); + etWalletPassword = (EditText) view.findViewById(R.id.etWalletPassword); + bGenerate = (Button) view.findViewById(R.id.bGenerate); + llAccept = (LinearLayout) view.findViewById(R.id.llAccept); + tvWalletMnemonic = (TextView) view.findViewById(R.id.tvWalletMnemonic); + bAccept = (Button) view.findViewById(R.id.bAccept); + + etWalletName.requestFocus(); + etWalletName.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + setGenerateEnabled(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + etWalletName.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(etWalletName, InputMethodManager.SHOW_IMPLICIT); + } + }); + etWalletName.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)) { + etWalletPassword.requestFocus(); + return false; + } + return false; + } + }); + + etWalletPassword.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(etWalletPassword, InputMethodManager.SHOW_IMPLICIT); + } + }); + etWalletPassword.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)) { + getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + generateWallet(); + return false; + } + return false; + } + }); + + bGenerate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + generateWallet(); + } + }); + + bAccept.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + acceptWallet(); + } + }); + bAccept.setEnabled(false); + + return view; + } + + private void generateWallet() { + String name = etWalletName.getText().toString(); + if (name.length() == 0) return; + File walletFile = new File(activityCallback.getStorageRoot(), name + ".keys"); + if (walletFile.exists()) { + Toast.makeText(getActivity(), getString(R.string.generate_wallet_exists), Toast.LENGTH_LONG).show(); + return; + } + String password = etWalletPassword.getText().toString(); + bGenerate.setEnabled(false); + activityCallback.onGenerate(name, password); + } + + private void acceptWallet() { + String name = etWalletName.getText().toString(); + String password = etWalletPassword.getText().toString(); + bAccept.setEnabled(false); + activityCallback.onAccept(name, password); + } + + private void setGenerateEnabled() { + bGenerate.setEnabled(etWalletName.length() > 0); + } + + @Override + public void onResume() { + super.onResume(); + setGenerateEnabled(); + etWalletName.requestFocus(); + InputMethodManager imgr = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imgr.showSoftInput(etWalletName, InputMethodManager.SHOW_IMPLICIT); + } + + public void showMnemonic(String mnemonic) { + setGenerateEnabled(); + if (mnemonic.length() > 0) { + tvWalletMnemonic.setText(mnemonic); + bAccept.setEnabled(true); + } else { + tvWalletMnemonic.setText(getActivity().getString(R.string.generate_seed)); + bAccept.setEnabled(false); + } + } + + GenerateFragment.Listener activityCallback; + + // Container Activity must implement this interface + public interface Listener { + void onGenerate(String name, String password); + + void onAccept(String name, String password); + + File getStorageRoot(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof GenerateFragment.Listener) { + this.activityCallback = (GenerateFragment.Listener) context; + } else { + throw new ClassCastException(context.toString() + + " must implement Listener"); + } + } + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index 9079c038..a5c3fe4c 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -19,6 +19,7 @@ package com.m2049r.xmrwallet; import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; +import android.app.FragmentTransaction; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -36,12 +37,15 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; +import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; +import com.m2049r.xmrwallet.service.MoneroHandlerThread; import com.m2049r.xmrwallet.util.Helper; import java.io.File; -public class LoginActivity extends Activity implements LoginFragment.LoginFragmentListener { +public class LoginActivity extends Activity + implements LoginFragment.Listener, GenerateFragment.Listener { static final String TAG = "LoginActivity"; static final int DAEMON_TIMEOUT = 500; // deamon must respond in 500ms @@ -62,7 +66,15 @@ public class LoginActivity extends Activity implements LoginFragment.LoginFragme // adapted from http://www.mkyong.com/android/android-prompt-user-input-dialog-example/ @Override - public void promptPassword(final String wallet) { + public void onWalletSelected(final String wallet) { + if (wallet.toLowerCase().startsWith("test")) { + startGenerateFragment(); + } else { + promptPassword(wallet); + } + } + + void promptPassword(final String wallet) { Context context = LoginActivity.this; LayoutInflater li = LayoutInflater.from(context); View promptsView = li.inflate(R.layout.prompt_password, null); @@ -130,7 +142,7 @@ public class LoginActivity extends Activity implements LoginFragment.LoginFragme } //////////////////////////////////////// - // LoginFragment.LoginFragmentListener + // LoginFragment.Listener //////////////////////////////////////// @Override public SharedPreferences getPrefs() { @@ -190,6 +202,126 @@ public class LoginActivity extends Activity implements LoginFragment.LoginFragme Fragment fragment = new LoginFragment(); getFragmentManager().beginTransaction() .add(R.id.fragment_container, fragment).commit(); - Log.d(TAG, "fragment added"); + Log.d(TAG, "LoginFragment added"); + } + + void startGenerateFragment() { + replaceFragment(new GenerateFragment()); + Log.d(TAG, "GenerateFragment placed"); + } + + void replaceFragment(Fragment newFragment) { + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.fragment_container, newFragment); + transaction.addToBackStack(null); + transaction.commit(); + } + + ////////////////////////////////////////// + // GenerateFragment.Listener + ////////////////////////////////////////// + static final String MNEMONIC_LANGUAGE = "English"; // see mnemonics/electrum-words.cpp for more + + @Override + public void onGenerate(final String name, final String password) { + final GenerateFragment genFragment = (GenerateFragment) + getFragmentManager().findFragmentById(R.id.fragment_container); + File newWalletFolder = new File(getStorageRoot(), ".new"); + if (!newWalletFolder.exists()) { + if (!newWalletFolder.mkdir()) { + Log.e(TAG, "Cannot create new wallet dir " + newWalletFolder.getAbsolutePath()); + genFragment.showMnemonic(""); + return; + } + } + if (!newWalletFolder.isDirectory()) { + Log.e(TAG, "New wallet dir " + newWalletFolder.getAbsolutePath() + "is not a directory"); + genFragment.showMnemonic(""); + return; + } + File cache = new File(newWalletFolder, name); + cache.delete(); + File keys = new File(newWalletFolder, name + ".keys"); + keys.delete(); + File address = new File(newWalletFolder, name + ".address.txt"); + address.delete(); + + if (cache.exists() || keys.exists() || address.exists()) { + Log.e(TAG, "Cannot remove all old wallet files: " + cache.getAbsolutePath()); + genFragment.showMnemonic(""); + return; + } + + final String newWalletPath = new File(newWalletFolder, name).getAbsolutePath(); + new Thread(null, + new Runnable() { + @Override + public void run() { + Log.d(TAG, "creating wallet " + newWalletPath); + Wallet newWallet = WalletManager.getInstance() + .createWallet(newWalletPath, password, MNEMONIC_LANGUAGE); + Log.d(TAG, "wallet created"); + Log.d(TAG, "Created " + newWallet.getAddress()); + Log.d(TAG, "Seed " + newWallet.getSeed() + "."); + final String mnemonic = newWallet.getSeed(); + newWallet.close(); + runOnUiThread(new Runnable() { + public void run() { + genFragment.showMnemonic(mnemonic); + } + }); + } + } + , "CreateWallet", MoneroHandlerThread.THREAD_STACK_SIZE).start(); + } + + @Override + public void onAccept(final String name, final String password) { + File newWalletFolder = new File(getStorageRoot(), ".new"); + if (!newWalletFolder.isDirectory()) { + Log.e(TAG, "New wallet dir " + newWalletFolder.getAbsolutePath() + "is not a directory"); + return; + } + final String newWalletPath = new File(newWalletFolder, name).getAbsolutePath(); + new Thread(null, + new Runnable() { + @Override + public void run() { + Log.d(TAG, "opening wallet " + newWalletPath); + Wallet newWallet = WalletManager.getInstance() + .openWallet(newWalletPath, password); + Wallet.Status status = newWallet.getStatus(); + Log.d(TAG, "wallet opened " + newWallet.getStatus()); + if (status != Wallet.Status.Status_Ok) { + Log.e(TAG, "New wallet is " + status.toString()); + runOnUiThread(new Runnable() { + public void run() { + Toast.makeText(LoginActivity.this, + getString(R.string.generate_wallet_create_failed), Toast.LENGTH_SHORT).show(); + } + }); + return; + } + final String walletPath = new File(getStorageRoot(), name).getAbsolutePath(); + final boolean rc = newWallet.store(walletPath); + Log.d(TAG, "wallet stored with rc=" + rc); + newWallet.close(); + Log.d(TAG, "wallet closed"); + runOnUiThread(new Runnable() { + public void run() { + if (rc) { + getFragmentManager().popBackStack(); + Toast.makeText(LoginActivity.this, + getString(R.string.generate_wallet_created), Toast.LENGTH_SHORT).show(); + } else { + Log.e(TAG, "Wallet store failed to " + walletPath); + Toast.makeText(LoginActivity.this, + getString(R.string.generate_wallet_create_failed), Toast.LENGTH_SHORT).show(); + } + } + }); + } + } + , "AcceptWallet", MoneroHandlerThread.THREAD_STACK_SIZE).start(); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index f925d08a..0b3e515f 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -45,38 +45,49 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.ArrayList; +import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Set; +import java.util.TreeSet; public class LoginFragment extends Fragment { private static final String TAG = "LoginFragment"; + public static final int WALLETNAME_PREAMBLE_LENGTH = "[123456] ".length(); + ListView listView; - List walletList = new ArrayList<>(); + Set walletList = new TreeSet<>(new Comparator() { + @Override + public int compare(String o1, String o2) { + return o1.substring(WALLETNAME_PREAMBLE_LENGTH).toLowerCase() + .compareTo(o2.substring(WALLETNAME_PREAMBLE_LENGTH).toLowerCase()); + } + }); List displayedList = new ArrayList<>(); ToggleButton tbMainNet; EditText etDaemonAddress; - LoginFragment.LoginFragmentListener activityCallback; + Listener activityCallback; // Container Activity must implement this interface - public interface LoginFragmentListener { + public interface Listener { SharedPreferences getPrefs(); File getStorageRoot(); - void promptPassword(final String wallet); + void onWalletSelected(final String wallet); } @Override public void onAttach(Context context) { super.onAttach(context); - if (context instanceof LoginFragment.LoginFragmentListener) { - this.activityCallback = (LoginFragment.LoginFragmentListener) context; + if (context instanceof Listener) { + this.activityCallback = (Listener) context; } else { throw new ClassCastException(context.toString() - + " must implement WalletFragmentListener"); + + " must implement Listener"); } } @@ -148,14 +159,13 @@ public class LoginFragment extends Fragment { } String itemValue = (String) listView.getItemAtPosition(position); - if ((isMainNet() && itemValue.charAt(1) != '4') - || (!isMainNet() && itemValue.charAt(1) != '9')) { + 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; } - final int preambleLength = "[123456] ".length(); - if (itemValue.length() <= (preambleLength)) { + if (itemValue.length() <= (WALLETNAME_PREAMBLE_LENGTH)) { Toast.makeText(getActivity(), getString(R.string.panic), Toast.LENGTH_LONG).show(); return; } @@ -167,8 +177,8 @@ public class LoginFragment extends Fragment { // looking good savePrefs(false); - String wallet = itemValue.substring(preambleLength); - activityCallback.promptPassword(wallet); + String wallet = itemValue.substring(WALLETNAME_PREAMBLE_LENGTH); + activityCallback.onWalletSelected(wallet); } }); loadList(); @@ -177,9 +187,10 @@ public class LoginFragment extends Fragment { private void filterList() { displayedList.clear(); - char x = isMainNet() ? '4' : '9'; + String x = isMainNet() ? "4" : "9A"; for (String s : walletList) { - if (s.charAt(1) == x) displayedList.add(s); + Log.d(TAG, "filtering " + s); + if (x.indexOf(s.charAt(1)) >= 0) displayedList.add(s); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 40ef7122..8e101015 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -31,7 +31,7 @@ import android.widget.Toast; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.service.WalletService; -public class WalletActivity extends Activity implements WalletFragment.WalletFragmentListener, +public class WalletActivity extends Activity implements WalletFragment.Listener, WalletService.Observer { private static final String TAG = "WalletActivity"; @@ -212,7 +212,7 @@ public class WalletActivity extends Activity implements WalletFragment.WalletFra } ////////////////////////////////////////// - // WalletFragment.WalletFragmentListener + // WalletFragment.Listener ////////////////////////////////////////// @Override diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java index 50791bdd..ee4a636d 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -188,10 +188,10 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O connectionStatusView.setText(net + " " + daemonConnected.toString().substring(17)); } - WalletFragmentListener activityCallback; + Listener activityCallback; // Container Activity must implement this interface - public interface WalletFragmentListener { + public interface Listener { boolean hasBoundService(); Wallet.ConnectionStatus getConnectionStatus(); @@ -205,15 +205,11 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O @Override public void onAttach(Context context) { super.onAttach(context); - if (context instanceof WalletFragmentListener) { - this.activityCallback = (WalletFragmentListener) context; + if (context instanceof Listener) { + this.activityCallback = (Listener) context; } else { throw new ClassCastException(context.toString() - + " must implement WalletFragmentListener"); + + " must implement Listener"); } } - - private void runOnUiThread(Runnable runnable) { - if (isAdded()) getActivity().runOnUiThread(runnable); - } } 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 2757877f..54782cf2 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java @@ -57,7 +57,7 @@ public class WalletManager { private void manageWallet(String walletId, Wallet wallet) { if (getWallet(walletId) != null) { - throw new IllegalStateException("Wallet already under management!"); + throw new IllegalStateException(walletId + " already under management!"); } Log.d(TAG, "Managing " + walletId); managedWallets.put(walletId, wallet); @@ -65,7 +65,7 @@ public class WalletManager { private void unmanageWallet(String walletId) { if (getWallet(walletId) == null) { - throw new IllegalStateException("Wallet not under management!"); + throw new IllegalStateException(walletId + " not under management!"); } Log.d(TAG, "Unmanaging " + walletId); managedWallets.remove(walletId); diff --git a/app/src/main/res/layout/gen_fragment.xml b/app/src/main/res/layout/gen_fragment.xml new file mode 100644 index 00000000..6a6edbc1 --- /dev/null +++ b/app/src/main/res/layout/gen_fragment.xml @@ -0,0 +1,94 @@ + + + + + + + + + +