package com.greenaddress.greenbits.ui; import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.net.Uri; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.widget.Toolbar; import android.text.Html; import android.util.Log; import android.view.ViewGroup; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.TextView; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.blockstream.libwally.Wally; import com.google.common.util.concurrent.FutureCallback; import com.greenaddress.greenapi.Network; import com.greenaddress.greenbits.GaService; import com.greenaddress.greenbits.ui.monitor.NetworkMonitorActivity; import com.greenaddress.greenbits.ui.preferences.SettingsActivity; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import org.bitcoinj.core.DumpedPrivateKey; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.TransactionSignature; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Observable; import java.util.Observer; import de.schildbach.wallet.ui.ScanActivity; // Problem with the above is that in the horizontal orientation the tabs don't go in the top bar public class TabbedMainActivity extends GaActivity implements Observer { private static final String TAG = TabbedMainActivity.class.getSimpleName(); public static final int REQUEST_SEND_QR_SCAN = 0, REQUEST_SWEEP_PRIVKEY = 1, REQUEST_BITCOIN_URL_LOGIN = 2, REQUEST_SETTINGS = 3, REQUEST_TX_DETAILS = 4, REQUEST_SEND_QR_SCAN_EXCHANGER = 5; private ViewPager mViewPager; private Menu mMenu; private Boolean mInternalQr = false; private String mSendAmount = null; private MaterialDialog mSegwitDialog; private final Runnable mSegwitCB = new Runnable() { public void run() { mSegwitDialog = null; } }; private MaterialDialog mSubaccountDialog; private final Runnable mSubaccountCB = new Runnable() { public void run() { mDialogCB.run(); mSubaccountDialog = null; } }; private final Runnable mDialogCB = new Runnable() { public void run() { setBlockWaitDialog(false); } }; private final Observer mTwoFactorObserver = new Observer() { @Override public void update(final Observable o, final Object data) { runOnUiThread(new Runnable() { public void run() { onTwoFactorConfigChange(); } }); } }; static boolean isBitcoinScheme(final Intent intent) { final Uri uri = intent.getData(); return uri != null && uri.getScheme() != null && uri.getScheme().equals("bitcoin"); } @Override protected void onCreateWithService(final Bundle savedInstanceState) { final Intent intent = getIntent(); mInternalQr = intent.getBooleanExtra("internal_qr", false); mSendAmount = intent.getStringExtra("sendAmount"); final boolean isBitcoinUri = isBitcoinScheme(intent) || intent.hasCategory(Intent.CATEGORY_BROWSABLE) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction()); if (isBitcoinUri && !mService.isLoggedOrLoggingIn()) { // Not logged in, force the user to login final Intent login = new Intent(this, RequestLoginActivity.class); startActivityForResult(login, REQUEST_BITCOIN_URL_LOGIN); return; } launch(isBitcoinUri); } private void onTwoFactorConfigChange() { final Map<?, ?> twoFacConfig = mService.getTwoFactorConfig(); if (twoFacConfig == null) return; if (!((Boolean) twoFacConfig.get("any")) && !mService.cfg().getBoolean("hideTwoFacWarning", false)) { final Snackbar snackbar = Snackbar .make(findViewById(R.id.main_content), getString(R.string.noTwoFactorWarning), Snackbar.LENGTH_INDEFINITE) .setActionTextColor(Color.RED) .setAction(getString(R.string.set2FA), new View.OnClickListener() { @Override public void onClick(final View v) { startActivityForResult(new Intent(TabbedMainActivity.this, SettingsActivity.class), REQUEST_SETTINGS); } }); final View snackbarView = snackbar.getView(); snackbarView.setBackgroundColor(Color.DKGRAY); final TextView textView = UI.find(snackbarView, android.support.design.R.id.snackbar_text); textView.setTextColor(Color.WHITE); snackbar.show(); } } private String formatValuePostfix(final Coin value) { final String formatted = UI.setCoinText(mService, null, null, value); return String.format("%s %s", formatted, mService.getBitcoinUnit()); } private void setAccountTitle(final int subAccount) { final boolean doLookup = subAccount != 0 && mService.haveSubaccounts(); final Map<String, ?> details; details = doLookup ? mService.findSubaccount(subAccount) : null; final String accountName; if (details == null) accountName = getResources().getString(R.string.main_account); else accountName = (String) details.get("name"); if (!mService.showBalanceInTitle()) { setTitle(accountName); return; } Coin balance = mService.getCoinBalance(subAccount); if (balance == null) balance = Coin.ZERO; setTitle(formatValuePostfix(balance) + " (" + accountName + ')'); } private void setBlockWaitDialog(final boolean doBlock) { getPagerAdapter().setBlockWaitDialog(doBlock); } private void configureSubaccountsFooter(final int subAccount) { setAccountTitle(subAccount); if (!mService.haveSubaccounts()) return; final FloatingActionButton fab = UI.find(this, R.id.fab); UI.show(fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { setBlockWaitDialog(true); final ArrayList subaccounts = mService.getSubaccounts(); final int subaccount_len = subaccounts.size() + 1; final ArrayList<String> names = new ArrayList<>(subaccount_len); final ArrayList<Integer> pointers = new ArrayList<>(subaccount_len); names.add(getResources().getString(R.string.main_account)); pointers.add(0); for (final Object s : subaccounts) { final Map<String, ?> m = (Map) s; names.add((String) m.get("name")); pointers.add((Integer) m.get("pointer")); } final AccountItemAdapter adapter = new AccountItemAdapter(names, pointers, mService); mSubaccountDialog = new MaterialDialog.Builder(TabbedMainActivity.this) .title(R.string.footerAccount) .adapter(adapter, null) .show(); UI.setDialogCloseHandler(mSubaccountDialog, mSubaccountCB); adapter.setCallback(new AccountItemAdapter.OnAccountSelected() { @Override public void onAccountSelected(final int account) { mSubaccountDialog.dismiss(); final int pointer = pointers.get(account); if (pointer == mService.getCurrentSubAccount()) return; setAccountTitle(pointer); onSubaccountUpdate(pointer); } }); } }); } private void onSubaccountUpdate(final int subAccount) { mService.setCurrentSubAccount(subAccount); final Intent data = new Intent("fragmentupdater"); data.putExtra("sub", subAccount); sendBroadcast(data); } @SuppressLint("NewApi") // NdefRecord#toUri disabled for API < 16 private void launch(final boolean isBitcoinUri) { setContentView(R.layout.activity_tabbed_main); final Toolbar toolbar = UI.find(this, R.id.toolbar); setSupportActionBar(toolbar); // Set up the action bar. final SectionsPagerAdapter sectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); // Set up the ViewPager with the sections adapter. mViewPager = UI.find(this, R.id.container); // Keep all of our tabs in memory while paging. This helps any races // left where broadcasts/callbacks are called on the pager when its not // shown. mViewPager.setOffscreenPageLimit(3); // Re-show our 2FA warning if config is changed to remove all methods // Fake a config change to show the warning if no current 2FA method mTwoFactorObserver.update(null, null); configureSubaccountsFooter(mService.getCurrentSubAccount()); // by default go to center tab int goToTab = 1; if (isBitcoinUri) { // go to send page tab goToTab = 2; // Started by clicking on a bitcoin URI, show the send tab initially. if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) { mViewPager.setTag(R.id.tag_bitcoin_uri, getIntent().getData()); } else { // NdefRecord#toUri not available in API < 16 if (Build.VERSION.SDK_INT > 16) { final Parcelable[] rawMessages; rawMessages = getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); for (final Parcelable parcel : rawMessages) { final NdefMessage ndefMsg = (NdefMessage) parcel; for (final NdefRecord record : ndefMsg.getRecords()) if (record.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(record.getType(), NdefRecord.RTD_URI)) { mViewPager.setTag(R.id.tag_bitcoin_uri, record.toUri()); } } } } // if arrives from internal QR scan if (mInternalQr) { mViewPager.setTag(R.id.internal_qr, "internal_qr"); } if (mSendAmount != null) { mViewPager.setTag(R.id.tag_amount, mSendAmount); } mInternalQr = false; mSendAmount = null; } // set adapter and tabs only after all setTag in ViewPager container mViewPager.setAdapter(sectionsPagerAdapter); mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(final int index) { sectionsPagerAdapter.onViewPageSelected(index); } }); final TabLayout tabLayout = UI.find(this, R.id.tabs); tabLayout.setupWithViewPager(mViewPager); mViewPager.setCurrentItem(goToTab); final boolean segwit = mService.getLoginData().get("segwit_server"); if (segwit && mService.isSegwitUnconfirmed()) { // The user has not yet enabled segwit. Opt them in and display // a dialog explaining how to opt-out. CB.after(mService.setUserConfig("use_segwit", true, false), new CB.Toast<Boolean>(this) { @Override public void onSuccess(final Boolean result) { TabbedMainActivity.this.runOnUiThread(new Runnable() { public void run() { mSegwitDialog = UI.popup(TabbedMainActivity.this, R.string.segwit_dialog_title, 0) .content(R.string.segwit_dialog_content).build(); UI.setDialogCloseHandler(mSegwitDialog, mSegwitCB); mSegwitDialog.show(); } }); } }); } } @Override public void onResumeWithService() { mService.addConnectionObserver(this); mService.addTwoFactorObserver(mTwoFactorObserver); if (mService.isForcedOff()) { // FIXME: Should pass flag to activity so it shows it was forced logged out startActivity(new Intent(this, FirstScreenActivity.class)); finish(); return; } final SectionsPagerAdapter adapter = getPagerAdapter(); setMenuItemVisible(mMenu, R.id.action_share, adapter != null && adapter.mSelectedPage == 0); } @Override public void onPauseWithService() { mService.deleteTwoFactorObserver(mTwoFactorObserver); mService.deleteConnectionObserver(this); if (mSubaccountDialog != null) mSubaccountDialog.dismiss(); if (mSegwitDialog != null) mSegwitDialog.dismiss(); } private final static int BIP38_FLAGS = (NetworkParameters.fromID(NetworkParameters.ID_MAINNET).equals(Network.NETWORK) ? Wally.BIP38_KEY_MAINNET : Wally.BIP38_KEY_TESTNET) | Wally.BIP38_KEY_COMPRESSED; @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); final TabbedMainActivity caller = TabbedMainActivity.this; switch (requestCode) { case REQUEST_TX_DETAILS: case REQUEST_SETTINGS: mService.updateBalance(mService.getCurrentSubAccount()); startActivity(new Intent(this, TabbedMainActivity.class)); finish(); break; case REQUEST_BITCOIN_URL_LOGIN: if (resultCode != RESULT_OK) { // The user failed to login after clicking on a bitcoin Uri finish(); return; } final boolean isBitcoinUri = true; launch(isBitcoinUri); break; case REQUEST_SWEEP_PRIVKEY: if (data == null) return; ECKey keyNonFinal = null; final String qrText = data.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); try { keyNonFinal = DumpedPrivateKey.fromBase58(Network.NETWORK, qrText).getKey(); } catch (final AddressFormatException e) { try { Wally.bip38_to_private_key(qrText, null, Wally.BIP38_KEY_COMPRESSED | Wally.BIP38_KEY_QUICK_CHECK); } catch (final IllegalArgumentException e2) { toast(R.string.invalid_key); return; } } final ECKey keyNonBip38 = keyNonFinal; final FutureCallback<Map<?, ?>> callback = new CB.Toast<Map<?, ?>>(caller) { @Override public void onSuccess(final Map<?, ?> sweepResult) { final View v = getLayoutInflater().inflate(R.layout.dialog_sweep_address, null, false); final TextView passwordPrompt = UI.find(v, R.id.sweepAddressPasswordPromptText); final TextView mainText = UI.find(v, R.id.sweepAddressMainText); final TextView addressText = UI.find(v, R.id.sweepAddressAddressText); final EditText passwordEdit = UI.find(v, R.id.sweepAddressPasswordText); final Transaction txNonBip38; final String address; if (keyNonBip38 != null) { UI.hide(passwordPrompt, passwordEdit); txNonBip38 = getSweepTx(sweepResult); Coin outputsValue = Coin.ZERO; for (final TransactionOutput output : txNonBip38.getOutputs()) outputsValue = outputsValue.add(output.getValue()); final String valueStr = formatValuePostfix(outputsValue); mainText.setText(Html.fromHtml("Are you sure you want to sweep <b>all</b> (" + valueStr + ") funds from the address below?")); address = keyNonBip38.toAddress(Network.NETWORK).toString(); } else { passwordPrompt.setText(R.string.sweep_bip38_passphrase_prompt); txNonBip38 = null; // amount not known until decrypted mainText.setText(Html.fromHtml("Are you sure you want to sweep <b>all</b> funds from the password protected BIP38 key below?")); address = data.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); } addressText.setText(String.format("%s\n%s\n%s", address.substring(0, 12), address.substring(12, 24), address.substring(24))); final MaterialDialog.Builder builder = UI.popup(caller, R.string.sweepAddressTitle, R.string.sweep, R.string.cancel) .customView(v, true) .onPositive(new MaterialDialog.SingleButtonCallback() { Transaction tx; ECKey key; private void doSweep() { final ArrayList<String> scripts = (ArrayList) sweepResult.get("prevout_scripts"); final Integer outPointer = (Integer) sweepResult.get("out_pointer"); CB.after(mService.verifySpendableBy(tx.getOutputs().get(0), 0, outPointer), new CB.Toast<Boolean>(caller) { @Override public void onSuccess(final Boolean isSpendable) { if (!isSpendable) { caller.toast(R.string.err_tabbed_sweep_failed); return; } final List<byte[]> signatures = new ArrayList<>(); for (int i = 0; i < tx.getInputs().size(); ++i) { final byte[] script = Wally.hex_to_bytes(scripts.get(i)); final TransactionSignature sig; sig = tx.calculateSignature(i, key, script, Transaction.SigHash.ALL, false); signatures.add(sig.encodeToBitcoin()); } CB.after(mService.sendTransaction(signatures), new CB.Toast<String>(caller) { }); } }); } @Override public void onClick(final MaterialDialog dialog, final DialogAction which) { if (keyNonBip38 != null) { tx = txNonBip38; key = keyNonBip38; doSweep(); return; } try { final String password = UI.getText(passwordEdit); final byte[] passbytes = password.getBytes(); final byte[] decryptedPKey = Wally.bip38_to_private_key(qrText, passbytes, BIP38_FLAGS); key = ECKey.fromPrivate(decryptedPKey); CB.after(mService.prepareSweepSocial(key.getPubKey(), true), new CB.Toast<Map<?, ?>>(caller) { @Override public void onSuccess(final Map<?, ?> sweepResult) { tx = getSweepTx(sweepResult); doSweep(); } }); } catch (final IllegalArgumentException e) { caller.toast(R.string.invalid_passphrase); } } }); runOnUiThread(new Runnable() { public void run() { builder.build().show(); } }); } }; if (keyNonBip38 != null) CB.after(mService.prepareSweepSocial(keyNonBip38.getPubKey(), true), callback); else callback.onSuccess(null); break; } } private Transaction getSweepTx(final Map<?, ?> sweepResult) { return GaService.buildTransaction((String) sweepResult.get("tx")); } @Override public boolean onCreateOptionsMenu(final Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. final int id = mService.isWatchOnly() ? R.menu.watchonly : R.menu.main; getMenuInflater().inflate(id, menu); setMenuItemVisible(menu, R.id.action_network, !GaService.IS_ELEMENTS && mService.isSPVEnabled()); setMenuItemVisible(menu, R.id.action_sweep, !GaService.IS_ELEMENTS); final boolean isExchanger = mService.cfg().getBoolean("show_exchanger_menu", false); setMenuItemVisible(menu, R.id.action_exchanger, isExchanger); mMenu = menu; return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { final TabbedMainActivity caller = TabbedMainActivity.this; switch (item.getItemId()) { case R.id.action_settings: startActivityForResult(new Intent(caller, SettingsActivity.class), REQUEST_SETTINGS); return true; case R.id.action_exchanger: startActivity(new Intent(caller, MainExchanger.class)); return true; case R.id.action_sweep: final Intent scanner = new Intent(caller, ScanActivity.class); //New Marshmallow permissions paradigm final String[] perms = {"android.permission.CAMERA"}; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1 && checkSelfPermission(perms[0]) != PackageManager.PERMISSION_GRANTED) requestPermissions(perms, /*permsRequestCode*/ 200); else startActivityForResult(scanner, REQUEST_SWEEP_PRIVKEY); return true; case R.id.network_unavailable: return true; case R.id.action_share: getPagerAdapter().onOptionsItemSelected(item); return true; case R.id.action_logout: mService.disconnect(false); finish(); return true; case R.id.action_network: startActivity(new Intent(caller, NetworkMonitorActivity.class)); return true; case R.id.action_about: startActivity(new Intent(caller, AboutActivity.class)); return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { if (mViewPager.getCurrentItem() == 1) finish(); else mViewPager.setCurrentItem(1); } @Override public void update(final Observable observable, final Object data) { final GaService.State state = (GaService.State) data; if (state.isForcedOff()) { // FIXME: Should pass flag to activity so it shows it was forced logged out startActivity(new Intent(this, FirstScreenActivity.class)); } setMenuItemVisible(mMenu, R.id.network_unavailable, !state.isLoggedIn()); } private void handlePermissionResult(final int[] granted, final int action, final int msgId) { if (granted[0] == PackageManager.PERMISSION_GRANTED) startActivityForResult(new Intent(this, ScanActivity.class), action); else shortToast(msgId); } @Override public void onRequestPermissionsResult(final int requestCode, final String[] permissions, final int[] granted) { if (requestCode == 200) handlePermissionResult(granted, REQUEST_SWEEP_PRIVKEY, R.string.err_tabbed_sweep_requires_camera_permissions); else if (requestCode == 100) handlePermissionResult(granted, REQUEST_SEND_QR_SCAN, R.string.err_qrscan_requires_camera_permissions); } SectionsPagerAdapter getPagerAdapter() { return (SectionsPagerAdapter) mViewPager.getAdapter(); } /** * A {@link FragmentPagerAdapter} that returns a fragment corresponding to * one of the sections/tabs/pages. */ public class SectionsPagerAdapter extends FragmentPagerAdapter { private final SubaccountFragment[] mFragments = new SubaccountFragment[3]; public int mSelectedPage = -1; private int mInitialSelectedPage = -1; private boolean mInitialPage = true; public SectionsPagerAdapter(final FragmentManager fm) { super(fm); } @Override public Fragment getItem(final int index) { Log.d(TAG, "SectionsPagerAdapter -> getItem " + index); switch (index) { case 0: return new ReceiveFragment(); case 1: return new MainFragment(); case 2: return new SendFragment(); } return null; } @Override public Object instantiateItem(final ViewGroup container, final int index) { Log.d(TAG, "SectionsPagerAdapter -> instantiateItem " + index); mFragments[index] = (SubaccountFragment) super.instantiateItem(container, index); if (mInitialPage && index == mInitialSelectedPage) { // Call setPageSelected() on the first page, now that it is created Log.d(TAG, "SectionsPagerAdapter -> selecting first page " + index); mFragments[index].setPageSelected(true); mInitialSelectedPage = -1; mInitialPage = false; } return mFragments[index]; } @Override public void destroyItem(final ViewGroup container, final int index, final Object object) { Log.d(TAG, "SectionsPagerAdapter -> destroyItem " + index); if (index >=0 && index <=2 && mFragments[index] != null) { // Make sure the fragment is not kept alive and does not // try to process any callbacks it registered for. mFragments[index].detachObservers(); // Make sure any wait dialog being shown is dismissed mFragments[index].setPageSelected(false); mFragments[index] = null; } super.destroyItem(container, index, object); } @Override public int getCount() { // We don't show the send tab in watch only mode return mService.isWatchOnly() ? 2 : 3; } @Override public CharSequence getPageTitle(final int index) { final Locale l = Locale.getDefault(); switch (index) { case 0: return getString(R.string.receive_title).toUpperCase(l); case 1: return getString(R.string.main_title).toUpperCase(l); case 2: return getString(R.string.send_title).toUpperCase(l); } return null; } public void onViewPageSelected(final int index) { Log.d(TAG, "SectionsPagerAdapter -> onViewPageSelected " + index + " current is " + mSelectedPage + " initial " + mInitialPage); if (mInitialPage) mInitialSelectedPage = index; // Store so we can notify it when constructed if (index == mSelectedPage) return; // No change to the selected page // Un-select any old selected page if (mSelectedPage != -1 && mFragments[mSelectedPage] != null) mFragments[mSelectedPage].setPageSelected(false); // Select the current page mSelectedPage = index; if (mFragments[mSelectedPage] != null) mFragments[mSelectedPage].setPageSelected(true); setMenuItemVisible(mMenu, R.id.action_share, mSelectedPage == 0); } public void setBlockWaitDialog(final boolean doBlock) { for (final SubaccountFragment fragment : mFragments) if (fragment != null) fragment.setBlockWaitDialog(doBlock); } public void onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == R.id.action_share) if (mSelectedPage == 0 && mFragments[0] != null) mFragments[0].onShareClicked(); } } }