diff --git a/.idea/.gitignore b/.idea/.gitignore index a7c382ed..6faf0832 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1 +1,2 @@ workspace.xml +markdown-navigator* diff --git a/README.md b/README.md index c089a200..81106d8d 100644 --- a/README.md +++ b/README.md @@ -3,39 +3,35 @@ Another Android Monero Wallet ### QUICKSTART - Download APK (Release) and install it -- Copy over synced wallet (all three files) onto sdcard in directory Monerujo (created first time app is started) -- Start app (again) +- Run the App and select "Generate Wallet" to create a new wallet or recover a wallet +- Advanced users could copy over synced wallet files (all files) onto sdcard in directory Monerujo (created first time App is started) - see the [FAQ](doc/FAQ.md) ### Disclaimer -This is my first serious Android App. +You may loose all your Moneroj if you use this App. ### Random Notes - Based off monero v0.10.3.1 with pull requests #2238, #2239 and #2289 applied => so can be used in mainnet! - currently only android32 -- currently only use is checking incoming/outgoing transactions -- works in testnet & mainnet (please use your own daemons) -- takes forever to sync on mainnet (even with own daemon) - due to 32-bit architecture +- ~~currently only use is checking incoming/outgoing transactions~~ +- works in testnet & mainnet +- takes forever to sync due to 32-bit architecture - use your own daemon - it's easy - screen stays on until first sync is complete -- saves wallet only on first sync +- saves wallet only on first sync and when sending transactions or editing notes - Monerujo means "Monero Wallet" according to https://www.reddit.com/r/Monero/comments/3exy7t/esperanto_corner/ ### TODO -- make it pretty +- wallet backup functions - adjust layout so we can use bigger font sizes (maybe show only 5 decimal places instead of 12 in main view) - review visibility of methods/classes -- sensible error dialogs (e.g. when no write permissions granted) instead of just crashing on purpose -- spend monero - not so difficult with wallet api +- more sensible error dialogs ~~(e.g. when no write permissions granted) instead of just crashing on purpose~~ - check licenses of included libraries; License Dialog -- ~~provide detailed build instructions for third party binaries~~ -- ~~sensible loading/saving progress bars instead of just freezing up~~ -- ~~figure out how to make it all flow better (loading/saving takes forever and does not run in background)~~ -- ~~currently loading in background thread produces segfaults in JNI~~ +- ~~make it pretty~~ (decided to go with "form follows function") +- ~~spend monero - not so difficult with wallet api~~ ### Issues -- ~~screen rotation crashes the app~~ -- ~~turning the display off/on during sync stops sync~~ +none :) ### HOW TO BUILD No need to build. Binaries are included: diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 56cefb6c..439bd6e7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,14 +19,13 @@ android:name=".WalletActivity" android:configChanges="orientation|keyboardHidden" android:label="@string/wallet_activity_name" - android:launchMode="singleTop" - android:screenOrientation="portrait"> + android:screenOrientation="portrait" /> + android:screenOrientation="portrait"> diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index 23b37aff..e4196ced 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -142,11 +142,11 @@ public class GenerateFragment extends Fragment { public void afterTextChanged(Editable editable) { if (etWalletMnemonic.length() > 0) { etWalletRestoreHeight.setVisibility(View.VISIBLE); - etWalletAddress.setVisibility(View.INVISIBLE); + etWalletAddress.setVisibility(View.GONE); } else { etWalletAddress.setVisibility(View.VISIBLE); if (etWalletAddress.length() == 0) { - etWalletRestoreHeight.setVisibility(View.INVISIBLE); + etWalletRestoreHeight.setVisibility(View.GONE); } else { etWalletRestoreHeight.setVisibility(View.VISIBLE); } @@ -193,10 +193,10 @@ public class GenerateFragment extends Fragment { etWalletMnemonic.setVisibility(View.INVISIBLE); etWalletRestoreHeight.setVisibility(View.VISIBLE); } else { - llRestoreKeys.setVisibility(View.INVISIBLE); + llRestoreKeys.setVisibility(View.GONE); etWalletMnemonic.setVisibility(View.VISIBLE); if (etWalletMnemonic.length() == 0) { - etWalletRestoreHeight.setVisibility(View.INVISIBLE); + etWalletRestoreHeight.setVisibility(View.GONE); } else { etWalletRestoreHeight.setVisibility(View.VISIBLE); } @@ -280,8 +280,8 @@ public class GenerateFragment extends Fragment { private boolean addressOk() { String address = etWalletAddress.getText().toString(); - // TODO only accept address from the correct net - return ((address.length() == 95) && ("49A".indexOf(address.charAt(0)) >= 0)); + boolean testnet = WalletManager.getInstance().isTestNet(); + return ((address.length() == 95) && ((testnet ? "9A" : "4").indexOf(address.charAt(0)) >= 0)); } private boolean viewKeyOk() { @@ -318,19 +318,19 @@ public class GenerateFragment extends Fragment { // figure out how we want to create this wallet // A. from scratch if ((seed.length() == 0) && (address.length() == 0)) { - bGenerate.setVisibility(View.INVISIBLE); + bGenerate.setVisibility(View.GONE); activityCallback.onGenerate(name, password); } else // B. from seed if (mnemonicOk()) { - bGenerate.setVisibility(View.INVISIBLE); + bGenerate.setVisibility(View.GONE); activityCallback.onGenerate(name, password, seed, height); } else // C. from keys if (addressOk() && viewKeyOk() && (spendKeyOk())) { String viewKey = etWalletViewKey.getText().toString(); String spendKey = etWalletSpendKey.getText().toString(); - bGenerate.setVisibility(View.INVISIBLE); + bGenerate.setVisibility(View.GONE); activityCallback.onGenerate(name, password, address, viewKey, spendKey, height); } else // D. none of the above :) diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index 29f770c9..758e1949 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -36,6 +36,7 @@ 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"; ProgressBar pbProgress; TextView tvWalletName; @@ -75,12 +76,15 @@ public class GenerateReviewFragment extends Fragment { showProgress(); Bundle b = getArguments(); - String name = b.getString("name"); - String password = b.getString("password"); String type = b.getString("type"); - tvWalletName.setText(new File(name).getName()); - show(name, password, 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); + } return view; } @@ -88,7 +92,7 @@ public class GenerateReviewFragment extends Fragment { String name = tvWalletName.getText().toString(); String password = tvWalletPassword.getText().toString(); bAccept.setEnabled(false); - activityCallback.onAccept(name, password); + acceptCallback.onAccept(name, password); } private void show(final String walletPath, final String password, final String type) { @@ -96,50 +100,55 @@ public class GenerateReviewFragment extends Fragment { new Runnable() { @Override public void run() { - Wallet wallet = WalletManager.getInstance().openWallet(walletPath, password); - final String name = wallet.getName(); - final String seed = wallet.getSeed(); - final String address = wallet.getAddress(); - final String view = wallet.getSecretViewKey(); - final String spend = wallet.isWatchOnly() ? "" : "not available - use seed for recovery"; - wallet.close(); - + final Wallet wallet = WalletManager.getInstance().openWallet(walletPath, password); getActivity().runOnUiThread(new Runnable() { public void run() { - if (type.equals(GenerateReviewFragment.VIEW_ACCEPT)) { - tvWalletPassword.setText(password); - bAccept.setVisibility(View.VISIBLE); - bAccept.setEnabled(true); - } - tvWalletName.setText(name); - tvWalletAddress.setText(address); - tvWalletMnemonic.setText(seed); - tvWalletViewKey.setText(view); - 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(); + show(wallet, password, type); } }); + wallet.close(); } } , "DetailsReview", MoneroHandlerThread.THREAD_STACK_SIZE).start(); - } - GenerateReviewFragment.Listener activityCallback; + 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); + } + 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)); + } + hideProgress(); + } + + GenerateReviewFragment.Listener acceptCallback = null; + GenerateReviewFragment.ListenerWithWallet walletCallback = null; public interface Listener { void onAccept(String name, String password); } + public interface ListenerWithWallet { + Wallet getWallet(); + } + @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof GenerateReviewFragment.Listener) { - this.activityCallback = (GenerateReviewFragment.Listener) context; + this.acceptCallback = (GenerateReviewFragment.Listener) context; + } else if (context instanceof GenerateReviewFragment.ListenerWithWallet) { + this.walletCallback = (GenerateReviewFragment.ListenerWithWallet) 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 25a04243..33f6870b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -16,7 +16,6 @@ package com.m2049r.xmrwallet; -import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.app.FragmentManager; @@ -29,6 +28,7 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -40,7 +40,6 @@ 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; @@ -63,6 +62,11 @@ public class LoginActivity extends AppCompatActivity if (savedInstanceState != null) { return; } + + Toolbar toolbar = (Toolbar) findViewById(R.id.tbLogin); + toolbar.setTitle(R.string.login_activity_name); + setSupportActionBar(toolbar); + if (Helper.getWritePermission(this)) { startLoginFragment(); } else { @@ -235,7 +239,6 @@ public class LoginActivity extends AppCompatActivity startReviewFragment(b); } - @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { Log.d(TAG, "onRequestPermissionsResult()"); @@ -420,9 +423,10 @@ public class LoginActivity extends AppCompatActivity boolean copyWallet(File dstDir, File srcDir, String name) { boolean success = false; try { - // TODO: the cache is corrupt if we recover (!!) - // TODO: the cache is ok if we immediately to a full refresh() - // TODO recoveryheight is ignored but not on watchonly wallet ?! - find out why + // 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"); diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 7747d03e..7b6dad0d 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -28,17 +28,21 @@ import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; import android.util.Log; +import android.view.View; import android.widget.Toast; import com.m2049r.xmrwallet.model.PendingTransaction; import com.m2049r.xmrwallet.model.TransactionInfo; import com.m2049r.xmrwallet.model.Wallet; +import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.service.WalletService; +import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.TxData; public class WalletActivity extends AppCompatActivity implements WalletFragment.Listener, - WalletService.Observer, SendFragment.Listener, TxFragment.Listener { + WalletService.Observer, SendFragment.Listener, TxFragment.Listener, GenerateReviewFragment.ListenerWithWallet { private static final String TAG = "WalletActivity"; static final int MIN_DAEMON_VERSION = 65544; @@ -46,6 +50,8 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. public static final String REQUEST_ID = "id"; public static final String REQUEST_PW = "pw"; + Toolbar tbWallet; + private boolean synced = false; @Override @@ -109,21 +115,29 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. Log.d(TAG, "onCreate()"); super.onCreate(savedInstanceState); setContentView(R.layout.wallet_activity); + if (savedInstanceState != null) { + return; + } + + tbWallet = (Toolbar) findViewById(R.id.tbWallet); + tbWallet.setTitle(R.string.app_name); + setSupportActionBar(tbWallet); + tbWallet.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onWalletDetails(); + } + }); Fragment walletFragment = new WalletFragment(); getFragmentManager().beginTransaction() .add(R.id.fragment_container, walletFragment).commit(); Log.d(TAG, "fragment added"); - if (savedInstanceState != null) { - return; - } - startWalletService(); Log.d(TAG, "onCreate() done."); } - public Wallet getWallet() { if (mBoundService == null) throw new IllegalStateException("WalletService not bound."); return mBoundService.getWallet(); @@ -256,7 +270,7 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. @Override public void setTitle(String title) { - super.setTitle(title); + tbWallet.setTitle(title); } @Override @@ -522,4 +536,13 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. transaction.addToBackStack(stackName); transaction.commit(); } -} + + private void onWalletDetails() { + Fragment fragment = getFragmentManager().findFragmentById(R.id.fragment_container); + if (fragment instanceof WalletFragment) { + Bundle extras = new Bundle(); + extras.putString("type", GenerateReviewFragment.VIEW_WALLET); + replaceFragment(new GenerateReviewFragment(), null, extras); + } + } +} \ 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 fafdf306..1f6dc5dd 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletFragment.java @@ -116,7 +116,7 @@ public class WalletFragment extends Fragment implements TransactionInfoAdapter.O updateStatus(wallet); } - public void onSynced() { // TODO watchonly + public void onSynced() { if (!activityCallback.isWatchOnly()) { bSend.setVisibility(View.VISIBLE); bSend.setEnabled(true); diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java index e8e60280..948805d4 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/TransactionInfoAdapter.java @@ -81,6 +81,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter data) { // TODO do stuff with data so we can really recycle elements (i.e. add only new tx) + // as the TransactionInfo items are always recreated, we cannot recycle this.infoItems.clear(); if (data != null) { Log.d(TAG, "setInfos " + data.size()); @@ -135,9 +136,8 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter lastTxCount) { - lastTxCount = txCount; // TODO maybe do this later + // update the transaction list only if we have more than before + lastTxCount = txCount; fullRefresh = true; } } @@ -173,10 +172,9 @@ public class WalletService extends Service { // these calls really connect to the daemon - wasting time daemonHeight = wallet.getDaemonBlockChainHeight(); if (daemonHeight > 0) { - // if we get a valid height, the obviously we are connected + // if we get a valid height, then obviously we are connected connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Connected; } else { - // TODO: or connectionStatus = wallet.getConnectionStatus(); ? connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Disconnected; } } @@ -485,7 +483,6 @@ public class WalletService extends Service { listener = null; } stopSelf(); - // TODO ensure the Looper & thread actually stop and go away? } private Wallet loadWallet(String walletName, String walletPassword) { diff --git a/app/src/main/res/layout/gen_fragment.xml b/app/src/main/res/layout/gen_fragment.xml index 0619d414..567abaf8 100644 --- a/app/src/main/res/layout/gen_fragment.xml +++ b/app/src/main/res/layout/gen_fragment.xml @@ -60,7 +60,7 @@ android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical" - android:visibility="invisible"> + android:visibility="gone"> + android:visibility="gone" />