Browse Source

Merge pull request #21 from CoboVault/btc_only_dev

multi watch-only-wallet support
V1.2.0-btc-release
soralit 4 years ago
committed by GitHub
parent
commit
fc6179fdd3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/build.gradle
  2. 1
      app/lint.xml
  3. 2
      app/src/main/assets/bundleMap.json
  4. 4
      app/src/main/assets/script/BTC.bundle_a521a1b83cf8627d0ce5.js
  5. 18
      app/src/main/java/com/cobo/cold/DataRepository.java
  6. 9
      app/src/main/java/com/cobo/cold/MainApplication.java
  7. 12
      app/src/main/java/com/cobo/cold/Utilities.java
  8. 44
      app/src/main/java/com/cobo/cold/callables/GetMasterFingerprintCallable.java
  9. 15
      app/src/main/java/com/cobo/cold/db/PresetData.java
  10. 6
      app/src/main/java/com/cobo/cold/db/dao/AccountDao.java
  11. 3
      app/src/main/java/com/cobo/cold/db/dao/CoinDao.java
  12. 3
      app/src/main/java/com/cobo/cold/db/dao/TxDao.java
  13. 12
      app/src/main/java/com/cobo/cold/db/entity/AccountEntity.java
  14. 2
      app/src/main/java/com/cobo/cold/encryption/interfaces/BASECONSTANTS.java
  15. 25
      app/src/main/java/com/cobo/cold/ui/MainActivity.java
  16. 3
      app/src/main/java/com/cobo/cold/ui/fragment/Constants.java
  17. 1
      app/src/main/java/com/cobo/cold/ui/fragment/PassphraseFragment.java
  18. 21
      app/src/main/java/com/cobo/cold/ui/fragment/SyncFragment.java
  19. 54
      app/src/main/java/com/cobo/cold/ui/fragment/main/AddressFragment.java
  20. 79
      app/src/main/java/com/cobo/cold/ui/fragment/main/AddressNumberPicker.java
  21. 279
      app/src/main/java/com/cobo/cold/ui/fragment/main/AssetFragment.java
  22. 153
      app/src/main/java/com/cobo/cold/ui/fragment/main/AssetListFragment.java
  23. 130
      app/src/main/java/com/cobo/cold/ui/fragment/main/ExportGenericXpubFragment.java
  24. 220
      app/src/main/java/com/cobo/cold/ui/fragment/main/ManageCoinFragment.java
  25. 171
      app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtListFragment.java
  26. 27
      app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtSignedTxFragment.java
  27. 105
      app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtTxConfirmFragment.java
  28. 32
      app/src/main/java/com/cobo/cold/ui/fragment/main/QRCodeScanFragment.java
  29. 2
      app/src/main/java/com/cobo/cold/ui/fragment/main/ReceiveCoinFragment.java
  30. 3
      app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java
  31. 80
      app/src/main/java/com/cobo/cold/ui/fragment/main/TxListFragment.java
  32. 97
      app/src/main/java/com/cobo/cold/ui/fragment/main/WalletInfoFragment.java
  33. 2
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumBroadcastTxFragment.java
  34. 50
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumExportFragment.java
  35. 14
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxnListFragment.java
  36. 177
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ExportXpubGuideFragment.java
  37. 59
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/SignedTxFragment.java
  38. 97
      app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/UnsignedTxFragment.java
  39. 99
      app/src/main/java/com/cobo/cold/ui/fragment/setting/ChooseWatchWalletFragment.java
  40. 5
      app/src/main/java/com/cobo/cold/ui/fragment/setting/FingerprintEnrollFragment.java
  41. 3
      app/src/main/java/com/cobo/cold/ui/fragment/setting/FingerprintManageFragment.java
  42. 24
      app/src/main/java/com/cobo/cold/ui/fragment/setting/ListPreferenceFragment.java
  43. 16
      app/src/main/java/com/cobo/cold/ui/fragment/setting/MainPreferenceFragment.java
  44. 76
      app/src/main/java/com/cobo/cold/ui/fragment/setup/SelectAddressFormatFragment.java
  45. 65
      app/src/main/java/com/cobo/cold/ui/fragment/setup/SetupSyncFragment.java
  46. 60
      app/src/main/java/com/cobo/cold/ui/fragment/setup/SetupWatchWalletFragment.java
  47. 2
      app/src/main/java/com/cobo/cold/ui/views/DrawerAdapter.java
  48. 163
      app/src/main/java/com/cobo/cold/viewmodel/AddAddressViewModel.java
  49. 7
      app/src/main/java/com/cobo/cold/viewmodel/CoinListViewModel.java
  50. 50
      app/src/main/java/com/cobo/cold/viewmodel/CoinViewModel.java
  51. 122
      app/src/main/java/com/cobo/cold/viewmodel/ElectrumViewModel.java
  52. 271
      app/src/main/java/com/cobo/cold/viewmodel/GlobalViewModel.java
  53. 127
      app/src/main/java/com/cobo/cold/viewmodel/PsbtViewModel.java
  54. 3
      app/src/main/java/com/cobo/cold/viewmodel/PublicKeyViewModel.java
  55. 84
      app/src/main/java/com/cobo/cold/viewmodel/QrScanViewModel.java
  56. 35
      app/src/main/java/com/cobo/cold/viewmodel/SetupVaultViewModel.java
  57. 60
      app/src/main/java/com/cobo/cold/viewmodel/SupportedWatchWallet.java
  58. 247
      app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java
  59. 59
      app/src/main/java/com/cobo/cold/viewmodel/WalletInfoViewModel.java
  60. 24
      app/src/main/java/com/cobo/cold/viewmodel/WatchWalletNotMatchException.java
  61. BIN
      app/src/main/res/drawable-xhdpi/electrum.png
  62. BIN
      app/src/main/res/drawable-xhdpi/sdcard.png
  63. 23
      app/src/main/res/drawable/text_button_bg.xml
  64. 83
      app/src/main/res/layout/add_address_bottom_sheet.xml
  65. 5
      app/src/main/res/layout/address_item.xml
  66. 90
      app/src/main/res/layout/asset_fragment.xml
  67. 38
      app/src/main/res/layout/dialog_bottom_sheet.xml
  68. 2
      app/src/main/res/layout/electrum_export.xml
  69. 2
      app/src/main/res/layout/electrum_tx_detail.xml
  70. 155
      app/src/main/res/layout/export_xpub.xml
  71. 16
      app/src/main/res/layout/export_xpub_guide.xml
  72. 2
      app/src/main/res/layout/file_list.xml
  73. 2
      app/src/main/res/layout/input_modal.xml
  74. 10
      app/src/main/res/layout/list_preference.xml
  75. 97
      app/src/main/res/layout/picker_dialog.xml
  76. 17
      app/src/main/res/layout/receive_fragment.xml
  77. 23
      app/src/main/res/layout/select_address_format.xml
  78. 22
      app/src/main/res/layout/setting_item_selectable.xml
  79. 36
      app/src/main/res/layout/setup_watch_wallet.xml
  80. 0
      app/src/main/res/layout/signed_tx.xml
  81. 10
      app/src/main/res/layout/sync.xml
  82. 12
      app/src/main/res/layout/sync_fragment.xml
  83. 26
      app/src/main/res/layout/tx_list.xml
  84. 77
      app/src/main/res/layout/tx_list_item.xml
  85. 244
      app/src/main/res/layout/wallet_info.xml
  86. 55
      app/src/main/res/layout/wallet_item.xml
  87. 12
      app/src/main/res/menu/asset_hasmore.xml
  88. 229
      app/src/main/res/navigation/nav_graph_main.xml
  89. 95
      app/src/main/res/navigation/nav_graph_setup.xml
  90. 91
      app/src/main/res/values-zh-rCN/strings.xml
  91. 113
      app/src/main/res/values/strings.xml
  92. 7
      app/src/main/res/values/styles.xml
  93. 3
      app/src/main/res/xml/main_preference.xml
  94. 10
      coinlib/src/main/java/com/cobo/coinlib/Util.java
  95. 53
      coinlib/src/main/java/com/cobo/coinlib/coins/AbsDeriver.java
  96. 5
      coinlib/src/main/java/com/cobo/coinlib/coins/AbsTx.java
  97. 54
      coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Btc.java
  98. 72
      coinlib/src/main/java/com/cobo/coinlib/coins/BTC/BtcImpl.java
  99. 68
      coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Deriver.java
  100. 34
      coinlib/src/main/java/com/cobo/coinlib/coins/SignPsbtResult.java

1
app/build.gradle

@ -195,6 +195,7 @@ preBuild {
XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
} catch (FileNotFoundException ignored) {
// nop, iml not found
println 'iml not found'
}
}
}

1
app/lint.xml

@ -26,4 +26,5 @@
<issue id="SetTextI18n" severity="ignore" />
<issue id="UnusedResources" severity="ignore" />
<issue id="UselessParent" severity="ignore"/>
<issue id="ContentDescription" severity="ignore"/>
</lint>

2
app/src/main/assets/bundleMap.json

@ -1,3 +1,3 @@
{
"BTC": "BTC.bundle_94e3c1790748ac7fc876.js"
"BTC": "BTC.bundle_a521a1b83cf8627d0ce5.js"
}

4
app/src/main/assets/script/BTC.bundle_94e3c1790748ac7fc876.js → app/src/main/assets/script/BTC.bundle_a521a1b83cf8627d0ce5.js

File diff suppressed because one or more lines are too long

18
app/src/main/java/com/cobo/cold/DataRepository.java

@ -17,6 +17,7 @@
package com.cobo.cold;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.lifecycle.LiveData;
@ -36,6 +37,7 @@ import java.util.List;
import java.util.stream.Collectors;
public class DataRepository {
@SuppressLint("StaticFieldLeak")
private static DataRepository sInstance;
private final AppDatabase mDb;
@ -106,6 +108,10 @@ public class DataRepository {
return mDb.coinDao().loadCoinSync(coinId, getBelongTo());
}
public LiveData<CoinEntity> loadCoin(final String coinId) {
return mDb.coinDao().loadCoin(coinId, getBelongTo());
}
public LiveData<List<AddressEntity>> loadAddress(String coinId) {
return mDb.addressDao().loadAddressForCoin(coinId, getBelongTo());
}
@ -130,6 +136,10 @@ public class DataRepository {
return mDb.txDao().loadElectrumTxsSync(coinId);
}
public List<TxEntity> loadWasabiTxsSync(String coinId) {
return mDb.txDao().loadWasabiTxsSync(coinId);
}
public LiveData<TxEntity> loadTx(String txId) {
return mDb.txDao().load(txId);
}
@ -192,6 +202,14 @@ public class DataRepository {
return mDb.accountDao().loadForCoin(coin.getId());
}
public AccountEntity loadAccountsByXpub(long id, String xpub) {
return mDb.accountDao().loadAccountByXpub(id, xpub);
}
public AccountEntity loadAccountsByPath(long id, String path) {
return mDb.accountDao().loadAccountByPath(id,path);
}
public CoinEntity loadCoinEntityByCoinCode(String coinCode) {
String coinId = Coins.coinIdFromCoinCode(coinCode);
return loadCoinSync(coinId);

9
app/src/main/java/com/cobo/cold/MainApplication.java

@ -17,6 +17,7 @@
package com.cobo.cold;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.BroadcastReceiver;
@ -24,7 +25,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@ -35,13 +35,11 @@ import com.cobo.cold.logging.FileLogger;
import com.cobo.cold.service.AttackCheckingService;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.UnlockActivity;
import com.cobo.cold.util.HashUtil;
import org.spongycastle.util.encoders.Hex;
import java.lang.ref.SoftReference;
public class MainApplication extends Application {
@SuppressLint("StaticFieldLeak")
private static MainApplication sApplication;
private AppExecutors mAppExecutors;
private SoftReference<Activity> topActivity;
@ -67,9 +65,6 @@ public class MainApplication extends Application {
});
initBackgroundCallBack();
ScriptLoader.init(this);
if (TextUtils.isEmpty(Utilities.getRandomSalt(this))) {
Utilities.setRandomSalt(this, Hex.toHexString(HashUtil.getNextSalt()));
}
IntentFilter mScreenOffFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
registerReceiver(mScreenOReceiver, mScreenOffFilter);

12
app/src/main/java/com/cobo/cold/Utilities.java

@ -36,7 +36,6 @@ import static com.cobo.cold.ui.fragment.setting.FingerprintPreferenceFragment.FI
public class Utilities {
public static final String PREFERENCE_SECRET = "secret";
public static final String PREFERENCE_KEY_PASSWORD = "password";
public static final String PREFERENCE_KEY_PATTERN = "pattern";
public static final String PREFERENCE_KEY_VAULT_CREATED = "vault_created";
public static final String PREFERENCE_KEY_LANGUAGE_SET = "language_set";
@ -52,7 +51,6 @@ public class Utilities {
public static final String PREFERENCE_KEY_MNEMONIC_COUNT = "mnemonic_count";
public static final String FINGERPRINT_CLICKED = "fingerprint_clicked";
public static final String PATTERN_LOCK_CLICKED = "pattern_lock_clicked";
public static final String PREFERENCE_KEY_RANDOM_SALT = "random_salt";
public static final String FINGERPRINT_PASSWORD = "fingerprint_password";
public static final String ATTACK_DETECTED = "attack_detected";
@ -119,16 +117,6 @@ public class Utilities {
sp.edit().putString(PREFERENCE_KEY_VAULT_ID, id).apply();
}
public static void setRandomSalt(Context context, String salt) {
SharedPreferences sp = context.getSharedPreferences(PREFERENCE_SECRET, MODE_PRIVATE);
sp.edit().putString(PREFERENCE_KEY_RANDOM_SALT, salt).apply();
}
public static String getRandomSalt(Context context) {
SharedPreferences sp = context.getSharedPreferences(PREFERENCE_SECRET, MODE_PRIVATE);
return sp.getString(PREFERENCE_KEY_RANDOM_SALT, "");
}
public static void setCurrentBelongTo(Context context, String s) {
SharedPreferences sp = context.getSharedPreferences(PREFERENCE_SECRET, MODE_PRIVATE);
sp.edit().putString(PREFERENCE_KEY_BELONG_TO, s).apply();

44
app/src/main/java/com/cobo/cold/callables/GetMasterFingerprintCallable.java

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.callables;
import com.cobo.cold.encryption.interfaces.CONSTANTS;
import com.cobo.cold.encryptioncore.base.Packet;
import com.cobo.cold.encryptioncore.base.Payload;
import java.util.concurrent.Callable;
public class GetMasterFingerprintCallable implements Callable<String> {
@Override
public String call() {
try {
final Callable<Packet> callable = new BlockingCallable(
new Packet.Builder(CONSTANTS.METHODS.GET_MASTER_FINGERPRINT).build());
final Packet result = callable.call();
final Payload payload = result.getPayload(CONSTANTS.TAGS.MASTER_FINGERPRINT);
if (payload != null) {
return payload.toHex();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}

15
app/src/main/java/com/cobo/cold/db/PresetData.java

@ -19,7 +19,6 @@ package com.cobo.cold.db;
import android.content.Context;
import com.cobo.coinlib.path.CoinPath;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.Utilities;
import com.cobo.cold.db.entity.AccountEntity;
@ -44,18 +43,12 @@ public class PresetData {
entity.setIndex(coin.coinIndex());
entity.setBelongTo(Utilities.getCurrentBelongTo(context));
entity.setAddressCount(0);
AccountEntity account = new AccountEntity();
String defaultHdPath = CoinPath.M()
.purpose(Coins.purposeNumber(entity.getCoinCode()))
.coinType(entity.getIndex())
.account(0)
.toString();
if (Coins.CURVE.ED25519 == getCurveByPath(defaultHdPath)) {
defaultHdPath += "/0'/0'";
for (String path : coin.accountPaths()) {
AccountEntity accountEntity = new AccountEntity();
accountEntity.setHdPath(path);
entity.addAccount(accountEntity);
}
account.setHdPath(defaultHdPath);
entity.addAccount(account);
return entity;
}

6
app/src/main/java/com/cobo/cold/db/dao/AccountDao.java

@ -35,6 +35,12 @@ public interface AccountDao {
@Query("SELECT * FROM accounts WHERE coinId=:id")
List<AccountEntity> loadForCoin(long id);
@Query("SELECT * FROM accounts WHERE coinId =:id AND exPub=:expub")
AccountEntity loadAccountByXpub(long id, String expub);
@Query("SELECT * FROM accounts WHERE coinId =:id AND hdPath=:path")
AccountEntity loadAccountByPath(long id, String path);
@Update
void update(AccountEntity account);
}

3
app/src/main/java/com/cobo/cold/db/dao/CoinDao.java

@ -48,6 +48,9 @@ public interface CoinDao {
@Query("SELECT * FROM coins WHERE coinId = :coinId AND belongTo = :belongTo")
CoinEntity loadCoinSync(String coinId, String belongTo);
@Query("SELECT * FROM coins WHERE coinId = :coinId AND belongTo = :belongTo")
LiveData<CoinEntity> loadCoin(String coinId, String belongTo);
@Update
int update(CoinEntity coinEntity);

3
app/src/main/java/com/cobo/cold/db/dao/TxDao.java

@ -36,6 +36,9 @@ public interface TxDao {
@Query("SELECT * FROM txs where coinId = :coinId and signId = 'electrum_sign_id' ORDER BY timeStamp DESC")
List<TxEntity> loadElectrumTxsSync(String coinId);
@Query("SELECT * FROM txs where coinId = :coinId and signId = 'wasabi_sign_id' ORDER BY timeStamp DESC")
List<TxEntity> loadWasabiTxsSync(String coinId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(TxEntity tx);

12
app/src/main/java/com/cobo/cold/db/entity/AccountEntity.java

@ -94,4 +94,16 @@ public class AccountEntity {
public void setCoinId(Long coinId) {
this.coinId = coinId;
}
@Override
public String toString() {
return "AccountEntity{" +
"id=" + id +
", hdPath='" + hdPath + '\'' +
", exPub='" + exPub + '\'' +
", addressLength=" + addressLength +
", isMultiSign=" + isMultiSign +
", coinId=" + coinId +
'}';
}
}

2
app/src/main/java/com/cobo/cold/encryption/interfaces/BASECONSTANTS.java

@ -37,6 +37,7 @@ public abstract class BASECONSTANTS {
int VERIFY_MNEMONIC = 0x0502;
int UPDATE_PASSPHRASE = 0x0303;
int GET_EXTENDED_PUBLICKEY = 0x0305;
int GET_MASTER_FINGERPRINT = 0x0308;
int WEB_AUTH = 0x0701;
int GET_UPDATE_KEY = 0x0702;
int REQUEST_UPDATE = 0x0108;
@ -87,6 +88,7 @@ public abstract class BASECONSTANTS {
int NEED_TOKEN = 0x0405;
int MESSAGE = 0x0406;
int MESSAGE_SIGNATURE = 0x0407;
int MASTER_FINGERPRINT = 0x020B;
}
public interface VALS {

25
app/src/main/java/com/cobo/cold/ui/MainActivity.java

@ -45,15 +45,12 @@ import com.cobo.cold.databinding.ActivityMainBinding;
import com.cobo.cold.fingerprint.FingerprintKit;
import com.cobo.cold.ui.common.FullScreenActivity;
import com.cobo.cold.ui.fragment.AboutFragment;
import com.cobo.cold.ui.fragment.SyncFragment;
import com.cobo.cold.ui.fragment.main.AssetListFragment;
import com.cobo.cold.ui.fragment.main.ManageCoinFragment;
import com.cobo.cold.ui.fragment.main.electrum.ElectrumTxnListFragment;
import com.cobo.cold.ui.fragment.main.AssetFragment;
import com.cobo.cold.ui.fragment.setting.SettingFragment;
import com.cobo.cold.ui.views.DrawerAdapter;
import com.cobo.cold.ui.views.FullScreenDrawer;
import com.cobo.cold.ui.views.UpdatingHelper;
import com.cobo.cold.viewmodel.ElectrumViewModel;
import com.cobo.cold.viewmodel.GlobalViewModel;
import java.util.Arrays;
import java.util.HashMap;
@ -75,7 +72,6 @@ public class MainActivity extends FullScreenActivity {
int currentFragmentIndex = R.id.drawer_wallet;
private DrawerAdapter drawerAdapter;
private ElectrumViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -92,7 +88,7 @@ public class MainActivity extends FullScreenActivity {
if (savedInstanceState == null) {
new UpdatingHelper(this);
}
viewModel = ViewModelProviders.of(this).get(ElectrumViewModel.class);
ViewModelProviders.of(this).get(GlobalViewModel.class);
}
private void initNavController() {
@ -178,14 +174,6 @@ public class MainActivity extends FullScreenActivity {
case R.id.drawer_wallet:
mNavController.navigateUp();
break;
case R.id.drawer_sync:
mNavController.navigateUp();
mNavController.navigate(R.id.action_to_syncFragment);
break;
case R.id.drawer_sdcard:
mNavController.navigateUp();
mNavController.navigate(R.id.action_to_txnListFragment);
break;
case R.id.drawer_settings:
mNavController.navigateUp();
mNavController.navigate(R.id.action_to_settingFragment);
@ -211,7 +199,7 @@ public class MainActivity extends FullScreenActivity {
} else {
NavDestination destination = mNavController.getCurrentDestination();
if (destination != null && destination.getLabel() != null) {
if (AssetListFragment.TAG.equals(destination.getLabel().toString())) {
if (AssetFragment.TAG.equals(destination.getLabel().toString())) {
return;
}
}
@ -288,10 +276,7 @@ public class MainActivity extends FullScreenActivity {
private final static Map<Integer, String> mMainFragments = new HashMap<>();
static {
mMainFragments.put(R.id.drawer_wallet, AssetListFragment.TAG);
mMainFragments.put(R.id.drawer_manage, ManageCoinFragment.TAG);
mMainFragments.put(R.id.drawer_sync, SyncFragment.TAG);
mMainFragments.put(R.id.drawer_sdcard, ElectrumTxnListFragment.TAG);
mMainFragments.put(R.id.drawer_wallet, AssetFragment.TAG);
mMainFragments.put(R.id.drawer_settings, SettingFragment.TAG);
mMainFragments.put(R.id.drawer_about, AboutFragment.TAG);
}

3
app/src/main/java/com/cobo/cold/ui/fragment/Constants.java

@ -23,6 +23,9 @@ public interface Constants {
String KEY_ID = "id";
String KEY_ADDRESS = "address";
String KEY_ADDRESS_NAME = "address_name";
String KEY_ADDRESS_PATH = "address_path";
String KEY_IS_CHANGE_ADDRESS = "is_change_address";
String KEY_ACCOUNT_HDPATH = "account_hd_path";
String KEY_TITLE = "title";
String KEY_NAV_ID = "nav_id";
String IS_FORCE = "is_force";

1
app/src/main/java/com/cobo/cold/ui/fragment/PassphraseFragment.java

@ -30,7 +30,6 @@ import android.widget.EditText;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.Observable;
import androidx.databinding.ObservableField;
import androidx.navigation.Navigation;
import com.cobo.cold.R;
import com.cobo.cold.Utilities;

21
app/src/main/java/com/cobo/cold/ui/fragment/SyncFragment.java

@ -44,11 +44,14 @@ public class SyncFragment extends BaseFragment<SyncFragmentBinding> {
@Override
protected void init(View view) {
mActivity.setSupportActionBar(mBinding.toolbar);
mBinding.toolbar.setNavigationOnClickListener(((MainActivity) mActivity)::toggleDrawer);
mBinding.toolbar.setTitle("");
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
viewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class);
subscribe(viewModel.getCoins());
if (mActivity instanceof MainActivity) {
mBinding.complete.setVisibility(View.GONE);
} else {
mBinding.complete.setOnClickListener(v -> navigate(R.id.action_to_setupCompleteFragment));
}
}
private void subscribe(LiveData<List<CoinEntity>> coins) {
@ -56,11 +59,13 @@ public class SyncFragment extends BaseFragment<SyncFragmentBinding> {
}
private void generateSyncData(List<CoinEntity> coinEntities) {
viewModel.generateSync(coinEntities).observe(this, sync -> {
if (!TextUtils.isEmpty(sync)) {
mBinding.sync.qrcodeLayout.qrcode.setData(sync);
}
});
if (coinEntities != null) {
viewModel.generateSync(coinEntities).observe(this, sync -> {
if (!TextUtils.isEmpty(sync)) {
mBinding.sync.qrcodeLayout.qrcode.setData(sync);
}
});
}
}
@Override

54
app/src/main/java/com/cobo/cold/ui/fragment/main/AddressFragment.java

@ -22,11 +22,11 @@ import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.R;
import com.cobo.cold.databinding.AddressFragmentBinding;
import com.cobo.cold.db.entity.AddressEntity;
@ -39,14 +39,19 @@ import java.util.Objects;
import static com.cobo.cold.ui.fragment.Constants.KEY_ADDRESS;
import static com.cobo.cold.ui.fragment.Constants.KEY_ADDRESS_NAME;
import static com.cobo.cold.ui.fragment.Constants.KEY_ADDRESS_PATH;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_CODE;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_ID;
import static com.cobo.cold.ui.fragment.Constants.KEY_ID;
import static com.cobo.cold.ui.fragment.Constants.KEY_IS_CHANGE_ADDRESS;
import static com.cobo.cold.viewmodel.GlobalViewModel.getAccount;
public class AddressFragment extends BaseFragment<AddressFragmentBinding> {
String query;
private String query;
private CoinViewModel viewModel;
private boolean isChangeAddress;
private String accountHdPath;
private List<AddressEntity> addressEntities;
private final AddressCallback mAddrCallback = new AddressCallback() {
@Override
public void onClick(AddressEntity addr) {
@ -58,6 +63,7 @@ public class AddressFragment extends BaseFragment<AddressFragmentBinding> {
data.putString(KEY_COIN_CODE, bundle.getString(KEY_COIN_CODE));
data.putString(KEY_ADDRESS, addr.getAddressString());
data.putString(KEY_ADDRESS_NAME, addr.getName());
data.putString(KEY_ADDRESS_PATH, addr.getPath());
navigate(R.id.action_to_receiveCoinFragment, data);
}
}
@ -70,18 +76,13 @@ public class AddressFragment extends BaseFragment<AddressFragmentBinding> {
private AddressAdapter mAddressAdapter;
public void exitEditAddressName() {
if (mAddressAdapter.isEditing()) {
mAddressAdapter.exitEdit();
}
}
public static Fragment newInstance(long id, @NonNull String coinId, @NonNull String coinCode) {
public static AddressFragment newInstance(@NonNull String coinId,
boolean isChange) {
AddressFragment fragment = new AddressFragment();
Bundle args = new Bundle();
args.putLong(KEY_ID, id);
args.putString(KEY_COIN_ID, coinId);
args.putString(KEY_COIN_CODE, coinCode);
args.putString(KEY_COIN_CODE, Coins.coinCodeFromCoinId(coinId));
args.putBoolean(KEY_IS_CHANGE_ADDRESS, isChange);
fragment.setArguments(args);
return fragment;
}
@ -113,28 +114,31 @@ public class AddressFragment extends BaseFragment<AddressFragmentBinding> {
@Override
protected void initData(Bundle savedInstanceState) {
Bundle data = Objects.requireNonNull(getArguments());
isChangeAddress = data.getBoolean(KEY_IS_CHANGE_ADDRESS);
accountHdPath = Objects.requireNonNull(getAccount(mActivity)).getPath();
Objects.requireNonNull(getParentFragment());
CoinViewModel.Factory factory = new CoinViewModel.Factory(mActivity.getApplication(),
data.getLong(KEY_ID),
data.getString(KEY_COIN_ID));
viewModel = ViewModelProviders.of(getParentFragment(), factory)
.get(CoinViewModel.class);
viewModel = ViewModelProviders.of(getParentFragment(), factory).get(CoinViewModel.class);
subscribeUi(viewModel.getAddress());
}
private void subscribeUi(LiveData<List<AddressEntity>> address) {
address.observe(this, entities -> mAddressAdapter.setItems(entities));
}
public void setQuery(String s) {
query = s;
mAddressAdapter.getFilter().filter(s);
if (addressEntities != null) {
updateAddressList(addressEntities);
}
address.observe(this, entities -> {
addressEntities = entities;
updateAddressList(entities);
});
}
public void enterSearch() {
if (mAddressAdapter != null) {
mAddressAdapter.enterSearch();
private void updateAddressList(List<AddressEntity> entities) {
List<AddressEntity> filteredEntity = viewModel.filterByAccountHdPath(entities, accountHdPath);
if (isChangeAddress) {
mAddressAdapter.setItems(viewModel.filterChangeAddress(filteredEntity));
} else {
mAddressAdapter.setItems(viewModel.filterReceiveAddress(filteredEntity));
}
}

79
app/src/main/java/com/cobo/cold/ui/fragment/main/AddressNumberPicker.java

@ -1,79 +0,0 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.DialogFragment;
import com.cobo.cold.R;
import com.cobo.cold.databinding.PickerDialogBinding;
import java.util.Objects;
import java.util.stream.IntStream;
public class AddressNumberPicker extends DialogFragment {
private NumberPickerCallback mCallback;
private static final int MIN = 0;
private static final int MAX = 8;
public AddressNumberPicker(){
}
public void setCallback(NumberPickerCallback mCallback) {
this.mCallback = mCallback;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Objects.requireNonNull(getDialog()).requestWindowFeature(STYLE_NO_TITLE);
PickerDialogBinding binding = DataBindingUtil.inflate(inflater, R.layout.picker_dialog,
container, false);
String[] displayValue = IntStream.range(MIN, MAX + 1)
.mapToObj(i -> String.valueOf(i + 1))
.toArray(String[]::new);
binding.setValue(1);
binding.picker.setValue(0);
binding.picker.setDisplayedValues(displayValue);
binding.picker.setMinValue(MIN);
binding.picker.setMaxValue(MAX);
binding.picker.setOnValueChangedListener((picker, oldVal, newVal) -> binding.setValue(newVal + 1));
binding.confirm.setOnClickListener(v -> {
dismiss();
if (mCallback != null) {
mCallback.onValueSet(binding.picker.getValue() + 1);
}
});
binding.cancel.setOnClickListener(v -> dismiss());
return binding.getRoot();
}
}

279
app/src/main/java/com/cobo/cold/ui/fragment/main/AssetFragment.java

@ -17,60 +17,58 @@
package com.cobo.cold.ui.fragment.main;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.Observable;
import androidx.databinding.ObservableField;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.R;
import com.cobo.cold.databinding.AddAddressBottomSheetBinding;
import com.cobo.cold.databinding.AssetFragmentBinding;
import com.cobo.cold.databinding.DialogBottomSheetBinding;
import com.cobo.cold.db.PresetData;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ProgressModalDialog;
import com.cobo.cold.viewmodel.AddAddressViewModel;
import com.cobo.cold.viewmodel.CoinViewModel;
import com.cobo.cold.viewmodel.PublicKeyViewModel;
import com.cobo.cold.viewmodel.SetupVaultViewModel;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.yanzhenjie.permission.AndPermission;
import com.yanzhenjie.permission.Permission;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import static androidx.fragment.app.FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_CODE;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_ID;
import static com.cobo.cold.ui.fragment.Constants.KEY_ID;
import static com.cobo.cold.viewmodel.GlobalViewModel.getAddressType;
public class AssetFragment extends BaseFragment<AssetFragmentBinding>
implements Toolbar.OnMenuItemClickListener, NumberPickerCallback {
implements NumberPickerCallback {
private final ObservableField<String> query = new ObservableField<>();
private boolean isInSearch;
public static final String TAG = "AssetFragment";
private Fragment[] fragments;
private boolean showPublicKey;
private String coinId;
private String coinCode;
private long id;
private AddressNumberPicker mAddressNumberPicker;
private boolean hasAddress;
private String[] title;
private SupportedWatchWallet watchWallet;
@Override
protected int setView() {
@ -79,57 +77,58 @@ public class AssetFragment extends BaseFragment<AssetFragmentBinding>
@Override
protected void init(View view) {
Bundle data = Objects.requireNonNull(getArguments());
coinId = data.getString(KEY_COIN_ID);
coinCode = data.getString(KEY_COIN_CODE);
id = data.getLong(KEY_ID);
showPublicKey = Coins.showPublicKey(coinCode);
mBinding.toolbar.inflateMenu(getMenuResId());
mBinding.toolbar.setOnMenuItemClickListener(this);
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
initSearchView();
initTabs();
coinId = Coins.BTC.coinId();
watchWallet = SupportedWatchWallet.getSupportedWatchWallet(mActivity);
mActivity.setSupportActionBar(mBinding.toolbar);
mBinding.toolbar.setNavigationOnClickListener(((MainActivity) mActivity)::toggleDrawer);
String walletName = watchWallet.getWalletName(mActivity);
mBinding.toolbar.setTitle(walletName);
mBinding.fab.setOnClickListener(v -> addAddress());
title = new String[]{ getString(R.string.tab_my_address), getString(R.string.tab_my_change_address)};
initViewPager();
}
private int getMenuResId() {
if (Coins.BTC.coinCode().equals(coinCode)) {
return R.menu.asset_hasmore;
}
return showPublicKey ? R.menu.asset_without_add : R.menu.asset;
private void addAddress() {
BottomSheetDialog dialog = new BottomSheetDialog(mActivity);
AddAddressBottomSheetBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.add_address_bottom_sheet,null,false);
String[] displayValue = IntStream.range(0, 9)
.mapToObj(i -> String.valueOf(i + 1))
.toArray(String[]::new);
binding.setValue(1);
binding.title.setText(getString(R.string.select_address_num, title[mBinding.tab.getSelectedTabPosition()]));
binding.close.setOnClickListener(v -> dialog.dismiss());
binding.picker.setValue(0);
binding.picker.setDisplayedValues(displayValue);
binding.picker.setMinValue(0);
binding.picker.setMaxValue(8);
binding.picker.setOnValueChangedListener((picker, oldVal, newVal) -> binding.setValue(newVal + 1));
binding.confirm.setOnClickListener(v-> {
onValueSet(binding.picker.getValue() + 1);
dialog.dismiss();
});
dialog.setContentView(binding.getRoot());
dialog.show();
}
private void initTabs() {
if (!showPublicKey) {
initViewPager();
} else {
PublicKeyViewModel viewModel = ViewModelProviders.of(this)
.get(PublicKeyViewModel.class);
Handler handler = new Handler();
AppExecutors.getInstance().diskIO().execute(() -> {
String address = viewModel.getAddress(coinId);
hasAddress = !TextUtils.isEmpty(address);
handler.post(this::initViewPager);
});
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.asset_hasmore, menu);
if (watchWallet == SupportedWatchWallet.COBO
|| watchWallet == SupportedWatchWallet.BLUE) {
menu.findItem(R.id.action_sdcard).setVisible(false);
}
super.onCreateOptionsMenu(menu, inflater);
}
private void initViewPager() {
String[] title = {showPublicKey && !hasAddress ? getString(R.string.tab_my_pubkey)
: getString(R.string.tab_my_address),
getString(R.string.tab_transaction_history)};
if (fragments == null) {
fragments = new Fragment[title.length];
if (showPublicKey) {
fragments[0] = PublicKeyFragment.newInstance(coinId);
} else {
fragments[0] = AddressFragment.newInstance(id, coinId, coinCode);
}
fragments[1] = TxListFragment.newInstance(coinId, coinCode);
fragments[0] = AddressFragment.newInstance(coinId, false);
fragments[1] = AddressFragment.newInstance(coinId, true);
}
mBinding.viewpager.setAdapter(new FragmentPagerAdapter(getChildFragmentManager(),
mBinding.viewpager.setAdapter(new FragmentStatePagerAdapter(getChildFragmentManager(),
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
@NonNull
@Override
@ -150,46 +149,23 @@ public class AssetFragment extends BaseFragment<AssetFragmentBinding>
mBinding.tab.setupWithViewPager(mBinding.viewpager);
}
private void initSearchView() {
mBinding.btnCancel.setOnClickListener(v -> exitSearch());
View.OnKeyListener backListener = (view, key_code, keyEvent) -> {
if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
if (key_code == KeyEvent.KEYCODE_BACK) {
if (isInSearch) {
exitSearch();
return true;
}
}
}
return false;
};
mBinding.search.setOnKeyListener(backListener);
query.set("");
mBinding.setQuery(query);
query.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
if (fragments[0] instanceof AddressFragment) {
AddressFragment addressFragment = (AddressFragment) fragments[0];
addressFragment.setQuery(query.get());
}
TxListFragment txListFragment = (TxListFragment) fragments[1];
txListFragment.setQuery(query.get());
}
});
}
@Override
protected void initData(Bundle savedInstanceState) {
CoinViewModel.Factory factory = new CoinViewModel.Factory(mActivity.getApplication(), id, coinId);
CoinViewModel.Factory factory = new CoinViewModel.Factory(mActivity.getApplication(), coinId);
CoinViewModel viewModel = ViewModelProviders.of(this, factory)
.get(CoinViewModel.class);
mBinding.setCoinViewModel(viewModel);
subscribeUi(viewModel);
checkAndAddNewCoins();
}
private void checkAndAddNewCoins() {
SetupVaultViewModel viewModel = ViewModelProviders.of(mActivity)
.get(SetupVaultViewModel.class);
AppExecutors.getInstance().diskIO().execute(()
-> viewModel.presetData(PresetData.generateCoins(mActivity), null)
);
}
private void subscribeUi(CoinViewModel viewModel) {
@ -197,84 +173,83 @@ public class AssetFragment extends BaseFragment<AssetFragmentBinding>
}
@Override
public boolean onMenuItemClick(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_search:
enterSearch();
break;
case R.id.action_add:
handleAddAddress();
break;
case R.id.action_more:
showBottomSheetMenu();
break;
case R.id.action_scan:
scanQrCode();
break;
case R.id.action_sdcard:
showFileList();
default:
break;
}
return true;
return super.onOptionsItemSelected(item);
}
private void handleAddAddress() {
if (fragments[0] instanceof AddressFragment) {
((AddressFragment) fragments[0]).exitEditAddressName();
}
if (mAddressNumberPicker == null) {
mAddressNumberPicker = new AddressNumberPicker();
mAddressNumberPicker.setCallback(this);
private void showFileList() {
switch (watchWallet) {
case ELECTRUM:
case GENERIC:
navigate(R.id.action_to_txnListFragment);
break;
case WASABI:
navigate(R.id.action_to_psbtListFragment);
break;
}
mAddressNumberPicker.show(mActivity.getSupportFragmentManager(), "");
}
private void scanQrCode() {
AndPermission.with(this)
.permission(Permission.CAMERA, Permission.READ_EXTERNAL_STORAGE)
.onGranted(permissions -> navigate(R.id.action_to_scan))
.onDenied(permissions -> {
Uri packageURI = Uri.parse("package:" + mActivity.getPackageName());
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Toast.makeText(mActivity, getString(R.string.scan_permission_denied), Toast.LENGTH_LONG).show();
}).start();
}
private void showBottomSheetMenu() {
BottomSheetDialog dialog = new BottomSheetDialog(mActivity);
DialogBottomSheetBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.dialog_bottom_sheet,null,false);
binding.addAddress.setOnClickListener(v-> {
handleAddAddress();
binding.exportXpub.setOnClickListener(v-> {
if (watchWallet == SupportedWatchWallet.GENERIC) {
navigate(R.id.action_to_export_xpub_generic);
} else {
navigate(R.id.action_to_export_xpub_guide);
}
dialog.dismiss();
});
binding.signHistory.setOnClickListener(v-> {
Bundle data = new Bundle();
data.putString(KEY_COIN_ID, coinId);
navigate(R.id.action_to_txList, data);
dialog.dismiss();
});
binding.exportXpubToElectrum.setOnClickListener(v-> {
navigate(R.id.action_to_electrum_guide);
binding.walletInfo.setOnClickListener(v-> {
navigate(R.id.action_to_walletInfoFragment);
dialog.dismiss();
});
dialog.setContentView(binding.getRoot());
dialog.show();
}
private void enterSearch() {
isInSearch = true;
if (fragments[0] != null && fragments[0] instanceof AddressFragment) {
((AddressFragment) fragments[0]).enterSearch();
}
mBinding.searchBar.setVisibility(View.VISIBLE);
mBinding.search.requestFocus();
InputMethodManager inputManager =
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputManager != null) {
inputManager.showSoftInput(mBinding.search, 0);
}
}
private void exitSearch() {
isInSearch = false;
mBinding.search.setText("");
mBinding.searchBar.setVisibility(View.INVISIBLE);
mBinding.search.clearFocus();
InputMethodManager inputManager =
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputManager != null) {
inputManager.hideSoftInputFromWindow(mBinding.search.getWindowToken(), 0);
}
}
@Override
public void onValueSet(int value) {
AddAddressViewModel.Factory factory = new AddAddressViewModel.Factory(mActivity.getApplication(),
id);
AddAddressViewModel viewModel = ViewModelProviders.of(this, factory)
AddAddressViewModel viewModel = ViewModelProviders.of(this)
.get(AddAddressViewModel.class);
ProgressModalDialog dialog = ProgressModalDialog.newInstance();
@ -283,14 +258,16 @@ public class AssetFragment extends BaseFragment<AssetFragmentBinding>
AppExecutors.getInstance().diskIO().execute(() -> {
CoinEntity coinEntity = viewModel.getCoin(coinId);
if (coinEntity != null) {
int addrCount = coinEntity.getAddressCount();
List<String> observableAddressNames = new ArrayList<>();
for (int i = addrCount; i < value + addrCount; i++) {
String name = coinEntity.getCoinCode() + "-" + (i + 1);
observableAddressNames.add(name);
int tabPosition = mBinding.tab.getSelectedTabPosition();
int changeIndex;
if (tabPosition == 0) {
changeIndex = 0;
} else {
changeIndex = 1;
}
viewModel.addAddress(observableAddressNames);
viewModel.addAddress(value, getAddressType(mActivity), changeIndex);
handler.post(() -> viewModel.getObservableAddState().observe(this, complete -> {
if (complete) {
handler.postDelayed(dialog::dismiss, 500);

153
app/src/main/java/com/cobo/cold/ui/fragment/main/AssetListFragment.java

@ -1,153 +0,0 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.R;
import com.cobo.cold.databinding.AssetListFragmentBinding;
import com.cobo.cold.db.PresetData;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.viewmodel.CoinListViewModel;
import com.cobo.cold.viewmodel.SetupVaultViewModel;
import com.yanzhenjie.permission.AndPermission;
import com.yanzhenjie.permission.Permission;
import java.util.List;
import java.util.stream.Collectors;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_CODE;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_ID;
import static com.cobo.cold.ui.fragment.Constants.KEY_ID;
import static com.cobo.cold.viewmodel.CoinListViewModel.coinEntityComparator;
public class AssetListFragment extends BaseFragment<AssetListFragmentBinding> {
public static final String TAG = "AssetListFragment";
private long startTime;
public static final int REQUEST_CODE_SCAN = 1000;
private CoinAdapter mCoinAdapter;
@Override
protected int setView() {
return R.layout.asset_list_fragment;
}
@Override
protected void init(View view) {
mActivity.setSupportActionBar(mBinding.toolbar);
mBinding.toolbar.setNavigationOnClickListener(((MainActivity) mActivity)::toggleDrawer);
mBinding.toolbar.setTitle("");
mCoinAdapter = new CoinAdapter(mActivity, mCoinClickCallback, false);
mBinding.assetList.setAdapter(mCoinAdapter);
}
@Override
protected void initData(Bundle savedInstanceState) {
CoinListViewModel mViewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class);
subscribeUi(mViewModel.getCoins());
checkAndAddNewCoins();
}
private void checkAndAddNewCoins() {
SetupVaultViewModel viewModel = ViewModelProviders.of(mActivity)
.get(SetupVaultViewModel.class);
AppExecutors.getInstance().diskIO().execute(()
-> viewModel.presetData(PresetData.generateCoins(mActivity), null)
);
}
private void subscribeUi(LiveData<List<CoinEntity>> coins) {
coins.observe(this, coinEntities -> {
if (coinEntities != null) {
List<CoinEntity> toShow = coinEntities
.stream()
.filter(CoinEntity::isShow)
.collect(Collectors.toList());
if (toShow.isEmpty()) {
mBinding.setIsEmpty(true);
} else {
mBinding.setIsEmpty(false);
toShow.sort(coinEntityComparator);
mCoinAdapter.setItems(toShow);
}
} else {
mBinding.setIsEmpty(true);
}
mBinding.executePendingBindings();
});
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.main, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_scan) {
AndPermission.with(this)
.permission(Permission.CAMERA, Permission.READ_EXTERNAL_STORAGE)
.onGranted(permissions -> {
startTime = System.currentTimeMillis();
Bundle bundle = new Bundle();
bundle.putLong("starttime", startTime);
navigate(R.id.action_to_scan, bundle);
})
.onDenied(permissions -> {
Uri packageURI = Uri.parse("package:" + mActivity.getPackageName());
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Toast.makeText(mActivity, getString(R.string.scan_permission_denied), Toast.LENGTH_LONG).show();
}).start();
return true;
}
return super.onOptionsItemSelected(item);
}
private final CoinClickCallback mCoinClickCallback = coin -> {
Bundle data = new Bundle();
data.putLong(KEY_ID, coin.getId());
data.putString(KEY_COIN_ID, coin.getCoinId());
data.putString(KEY_COIN_CODE, coin.getCoinCode());
navigate(R.id.action_to_assetFragment, data);
};
}

130
app/src/main/java/com/cobo/cold/ui/fragment/main/ExportGenericXpubFragment.java

@ -0,0 +1,130 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.databinding.DataBindingUtil;
import com.cobo.coinlib.Util;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ExportSdcardModalBinding;
import com.cobo.cold.databinding.ExportXpubBinding;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.SetupVaultActivity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.Storage;
import com.cobo.cold.viewmodel.GlobalViewModel;
import org.json.JSONException;
import org.json.JSONObject;
import static com.cobo.cold.viewmodel.GlobalViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.GlobalViewModel.getAccount;
import static com.cobo.cold.viewmodel.GlobalViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.GlobalViewModel.writeToSdcard;
public class ExportGenericXpubFragment extends BaseFragment<ExportXpubBinding> {
private JSONObject xpubInfo;
@Override
protected int setView() {
return R.layout.export_xpub;
}
@Override
protected void init(View view) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
try {
xpubInfo = GlobalViewModel.getXpubInfo(mActivity);
String exPub = xpubInfo.getString("ExtPubKey");
exPub = convertExtpub(exPub, getAccount(mActivity));
xpubInfo.put("ExtPubKey", exPub);
mBinding.qrcode.setData(xpubInfo.toString());
mBinding.expub.setText(exPub);
} catch (JSONException e) {
e.printStackTrace();
}
mBinding.addressType.setText(getString(R.string.master_xpub,
GlobalViewModel.getAddressFormat(mActivity)));
mBinding.done.setOnClickListener(v -> {
if (mActivity instanceof SetupVaultActivity) {
navigate(R.id.action_to_setupCompleteFragment);
} else {
MainActivity activity = (MainActivity) mActivity;
activity.getNavController().popBackStack(R.id.assetFragment, false);
}
});
mBinding.exportToSdcard.setOnClickListener(v -> {
Storage storage = Storage.createByEnvironment(mActivity);
if (storage == null || storage.getExternalDir() == null) {
showNoSdcardModal(mActivity);
} else {
ModalDialog modalDialog = ModalDialog.newInstance();
ExportSdcardModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.export_sdcard_modal, null, false);
binding.title.setText(R.string.export_xpub_text_file);
binding.fileName.setText(getFileName());
binding.actionHint.setText(R.string.electrum_import_xpub_action);
binding.cancel.setOnClickListener(vv -> modalDialog.dismiss());
binding.confirm.setOnClickListener(vv -> {
modalDialog.dismiss();
if (writeToSdcard(storage, xpubInfo.toString(), getFileName())) {
exportSuccess(mActivity, null);
}
});
modalDialog.setBinding(binding);
modalDialog.show(mActivity.getSupportFragmentManager(), "");
}
});
}
private String getFileName() {
Coins.Account account = GlobalViewModel.getAccount(mActivity);
switch (account) {
case SegWit:
return "p2wpkh-pubkey.txt";
case P2SH:
return "p2wpkh-p2sh-pubkey.txt";
case P2PKH:
return "p2pkh-pubkey.txt";
}
return "p2wpkh-p2sh-pubkey.txt";
}
@Override
protected void initData(Bundle savedInstanceState) {
}
private String convertExtpub(String xpub, Coins.Account account) {
if (account == Coins.Account.SegWit) {
return Util.convertXpubToZpub(xpub);
} else if (account == Coins.Account.P2SH) {
return Util.convertXpubToYpub(xpub);
} else {
return xpub;
}
}
}

220
app/src/main/java/com/cobo/cold/ui/fragment/main/ManageCoinFragment.java

@ -1,220 +0,0 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.databinding.Observable;
import androidx.databinding.ObservableField;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.MainApplication;
import com.cobo.cold.R;
import com.cobo.cold.config.FeatureFlags;
import com.cobo.cold.databinding.ManageCoinFragmentBinding;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.SetupVaultActivity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.viewmodel.CoinListViewModel;
import java.util.List;
import java.util.Objects;
import static com.cobo.cold.Utilities.IS_SETUP_VAULT;
import static com.cobo.cold.Utilities.IS_SET_PASSPHRASE;
import static com.cobo.cold.viewmodel.CoinListViewModel.coinEntityComparator;
public class ManageCoinFragment extends BaseFragment<ManageCoinFragmentBinding> {
private final ObservableField<String> query = new ObservableField<>();
public static final String TAG = "ManageCoinFragment";
private CoinAdapter mCoinAdapter;
private CoinListViewModel mViewModel;
private boolean hideConfirmAction = true;
private boolean isInSearch;
@Override
protected int setView() {
return R.layout.manage_coin_fragment;
}
@Override
protected void init(View view) {
mActivity.setSupportActionBar(mBinding.toolbar);
Bundle data = getArguments();
if (data != null && data.getBoolean(IS_SET_PASSPHRASE)) {
mBinding.toolbarTitle.setText(R.string.add_coins);
mBinding.toolbar.setNavigationIcon(new ColorDrawable(Color.TRANSPARENT));
mBinding.toolbar.setNavigationOnClickListener(null);
hideConfirmAction = false;
} else if (mActivity instanceof SetupVaultActivity) {
mBinding.toolbarTitle.setText(R.string.add_coins);
mBinding.toolbar.setNavigationIcon(new ColorDrawable(Color.TRANSPARENT));
mBinding.toolbar.setNavigationOnClickListener(null);
hideConfirmAction = false;
} else {
mBinding.toolbar.setNavigationOnClickListener(((MainActivity) mActivity)::toggleDrawer);
}
mBinding.toolbar.setTitle("");
mCoinAdapter = new CoinAdapter(mActivity, mCoinClickCallback, true);
mBinding.assetList.setAdapter(mCoinAdapter);
initSearchView();
}
private void initSearchView() {
mBinding.btnCancel.setOnClickListener(v -> exitSearch());
View.OnKeyListener backListener = (view, keyCode, keyEvent) -> {
if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (isInSearch) {
exitSearch();
return true;
}
}
}
return false;
};
mBinding.search.setOnKeyListener(backListener);
query.set("");
mBinding.setQuery(query);
query.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
mCoinAdapter.getFilter().filter(query.get());
}
});
mCoinAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
if (isInSearch) {
if (mCoinAdapter.getItems().size() == 0) {
mBinding.empty.setVisibility(View.VISIBLE);
mBinding.assetList.setVisibility(View.GONE);
} else {
mBinding.empty.setVisibility(View.GONE);
mBinding.assetList.setVisibility(View.VISIBLE);
}
}
}
});
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.manage, menu);
if (hideConfirmAction) {
menu.findItem(R.id.action_confirm).setVisible(false);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_confirm:
Bundle data = Objects.requireNonNull(getArguments());
if (data.getBoolean(IS_SETUP_VAULT)) {
// if (FeatureFlags.ENABLE_WHITE_LIST) {
// navigate(R.id.action_manageCoin_to_manageWhiteList, data);
// } else {
// navigate(R.id.action_manageCoinFragment_to_setupSyncFragment, data);
// }
} else {
startActivity(new Intent(mActivity, MainActivity.class));
}
break;
case R.id.action_search:
enterSearch();
break;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void initData(Bundle savedInstanceState) {
mViewModel = ViewModelProviders.of(this).get(CoinListViewModel.class);
if (mActivity instanceof SetupVaultActivity) {
subscribeUi(MainApplication.getApplication().getRepository().reloadCoins());
} else if (getArguments() != null && getArguments().getBoolean(IS_SET_PASSPHRASE)) {
subscribeUi(MainApplication.getApplication().getRepository().reloadCoins());
} else {
subscribeUi(mViewModel.getCoins());
}
}
private void subscribeUi(LiveData<List<CoinEntity>> coins) {
coins.observe(this, coinEntities -> {
if (coinEntities != null) {
coinEntities.sort(coinEntityComparator);
mCoinAdapter.setItems(coinEntities);
}
mBinding.executePendingBindings();
});
}
private final CoinClickCallback mCoinClickCallback = coin ->
AppExecutors.getInstance().diskIO()
.execute(() -> mViewModel.toggleCoin(coin));
private void enterSearch() {
isInSearch = true;
mBinding.searchBar.setVisibility(View.VISIBLE);
mBinding.search.requestFocus();
InputMethodManager inputManager =
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputManager != null) {
inputManager.showSoftInput(mBinding.search, 0);
}
}
private void exitSearch() {
isInSearch = false;
mBinding.search.setText("");
mBinding.searchBar.setVisibility(View.INVISIBLE);
mBinding.search.clearFocus();
mBinding.empty.setVisibility(View.GONE);
mBinding.assetList.setVisibility(View.VISIBLE);
InputMethodManager inputManager =
(InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputManager != null) {
inputManager.hideSoftInputFromWindow(mBinding.search.getWindowToken(), 0);
}
}
}

171
app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtListFragment.java

@ -0,0 +1,171 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ElectrumTxnBinding;
import com.cobo.cold.databinding.FileListBinding;
import com.cobo.cold.ui.common.BaseBindingAdapter;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.fragment.main.electrum.Callback;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.FileUtils;
import com.cobo.cold.update.utils.Storage;
import com.cobo.cold.viewmodel.PsbtViewModel;
import org.spongycastle.util.encoders.Base64;
import org.spongycastle.util.encoders.Hex;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.cobo.cold.viewmodel.GlobalViewModel.hasSdcard;
public class PsbtListFragment extends BaseFragment<FileListBinding>
implements Callback {
public static final String TAG = "ElectrumTxnListFragment";
public static final String PSBT_MAGIC_PREFIX = Hex.toHexString("psbt".getBytes(StandardCharsets.UTF_8));
private PsbtViewModel viewModel;
private TxnAdapter adapter;
private AtomicBoolean showEmpty;
@Override
protected int setView() {
return R.layout.file_list;
}
@Override
protected void init(View view) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
viewModel = ViewModelProviders.of(mActivity).get(PsbtViewModel.class);
adapter = new TxnAdapter(mActivity, this);
initViews();
}
private void initViews() {
showEmpty = new AtomicBoolean(false);
if (!hasSdcard(mActivity)) {
showEmpty.set(true);
mBinding.emptyTitle.setText(R.string.no_sdcard);
mBinding.emptyMessage.setText(R.string.no_sdcard_hint);
} else {
mBinding.list.setAdapter(adapter);
viewModel.loadUnsignPsbt().observe(this, files -> {
if (files.size() > 0) {
adapter.setItems(files);
} else {
showEmpty.set(true);
mBinding.emptyTitle.setText(R.string.no_unsigned_txn);
mBinding.emptyMessage.setText(R.string.no_unsigned_txn_hint);
}
updateUi();
});
}
updateUi();
}
private void updateUi() {
if (showEmpty.get()) {
mBinding.emptyView.setVisibility(View.VISIBLE);
mBinding.list.setVisibility(View.GONE);
} else {
mBinding.emptyView.setVisibility(View.GONE);
mBinding.list.setVisibility(View.VISIBLE);
}
}
@Override
protected void initData(Bundle savedInstanceState) {
}
@Override
public void onClick(String file) {
Storage storage = Storage.createByEnvironment(mActivity);
byte[] content = FileUtils.bufferlize(new File(storage.getExternalDir(), file));
if (content != null) {
String psbtBase64 = null;
if (isBase64Psbt(content)) {
psbtBase64 = new String(content);
} else if (Hex.toHexString(content).startsWith(PSBT_MAGIC_PREFIX)) {
psbtBase64 = Base64.toBase64String(content);
}
if (psbtBase64 != null) {
Bundle bundle = new Bundle();
bundle.putString("psbt_base64", psbtBase64);
navigate(R.id.action_to_psbtTxConfirmFragment, bundle);
return;
}
}
ModalDialog.showCommonModal(mActivity,
getString(R.string.electrum_decode_txn_fail),
getString(R.string.error_txn_file),
getString(R.string.confirm),
null);
}
private boolean isBase64Psbt(byte[] content) {
try {
byte[] data = Base64.decode(new String(content));
return Hex.toHexString(data).startsWith(PSBT_MAGIC_PREFIX);
}catch (Exception e) {
return false;
}
}
public static class TxnAdapter extends BaseBindingAdapter<String, ElectrumTxnBinding> {
private Callback callback;
TxnAdapter(Context context, Callback callback) {
super(context);
this.callback = callback;
}
@Override
protected int getLayoutResId(int viewType) {
return R.layout.electrum_txn;
}
@Override
protected void onBindItem(ElectrumTxnBinding binding, String item) {
binding.setFile(item);
binding.setCallback(callback);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
}
}
}

27
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumGuideFragment.java → app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtSignedTxFragment.java

@ -15,30 +15,25 @@
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
package com.cobo.cold.ui.fragment.main;
import android.os.Bundle;
import android.view.View;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ElectrumExportGuideBinding;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.db.entity.TxEntity;
import com.cobo.cold.ui.fragment.main.electrum.SignedTxFragment;
public class ElectrumGuideFragment extends BaseFragment<ElectrumExportGuideBinding> {
import static com.cobo.cold.ui.fragment.main.PsbtTxConfirmFragment.showExportPsbtDialog;
public class PsbtSignedTxFragment extends SignedTxFragment {
@Override
protected int setView() {
return R.layout.electrum_export_guide;
protected void displaySignResult(TxEntity txEntity) {
mBinding.txDetail.qr.setVisibility(View.GONE);
mBinding.txDetail.broadcastGuide.setVisibility(View.GONE);
}
@Override
protected void init(View view) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.export.setOnClickListener(v -> navigate(R.id.export_electrum_ypub));
}
@Override
protected void initData(Bundle savedInstanceState) {
protected void showExportDialog() {
showExportPsbtDialog(mActivity, txEntity.getTxId(),
txEntity.getSignedHex(), this::navigateUp);
}
}

105
app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtTxConfirmFragment.java

@ -0,0 +1,105 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ExportSdcardModalBinding;
import com.cobo.cold.ui.fragment.main.electrum.UnsignedTxFragment;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.ui.views.AuthenticateModal;
import com.cobo.cold.update.utils.FileUtils;
import com.cobo.cold.update.utils.Storage;
import java.io.File;
import java.util.Objects;
import static com.cobo.cold.viewmodel.GlobalViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.GlobalViewModel.hasSdcard;
import static com.cobo.cold.viewmodel.GlobalViewModel.showNoSdcardModal;
public class PsbtTxConfirmFragment extends UnsignedTxFragment {
private String psbtBase64;
@Override
protected void init(View view) {
super.init(view);
}
static void showExportPsbtDialog(AppCompatActivity activity, String txId, String psbt,
Runnable onExportSuccess) {
ModalDialog modalDialog = ModalDialog.newInstance();
ExportSdcardModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(activity),
R.layout.export_sdcard_modal, null, false);
String fileName = "signed_" + txId.substring(0, 8) + ".psbt";
binding.title.setText(R.string.export_signed_txn);
binding.fileName.setText(fileName);
binding.actionHint.setVisibility(View.GONE);
binding.cancel.setOnClickListener(vv -> modalDialog.dismiss());
binding.confirm.setOnClickListener(vv -> {
modalDialog.dismiss();
if (hasSdcard(activity)) {
Storage storage = Storage.createByEnvironment(activity);
File file = new File(Objects.requireNonNull(storage).getExternalDir(), fileName);
boolean result = FileUtils.writeString(file, psbt);
if (result) {
exportSuccess(activity, onExportSuccess);
}
} else {
showNoSdcardModal(activity);
}
});
modalDialog.setBinding(binding);
modalDialog.show(activity.getSupportFragmentManager(), "");
}
@Override
protected AuthenticateModal.OnVerify signWithVerifyInfo() {
return token -> {
viewModel.setToken(token);
viewModel.handleSignPsbt(psbtBase64);
subscribeSignState();
};
}
@Override
protected void parseTx() {
psbtBase64 = Objects.requireNonNull(getArguments()).getString("psbt_base64");
viewModel.parsePsbtBase64(psbtBase64);
}
protected void onSignSuccess() {
showExportPsbtDialog(mActivity, viewModel.getTxId(),
viewModel.getTxHex(), this::navigateUp);
viewModel.getSignState().removeObservers(this);
}
@Override
protected void initData(Bundle savedInstanceState) {
}
}

32
app/src/main/java/com/cobo/cold/ui/fragment/main/QRCodeScanFragment.java

@ -43,14 +43,15 @@ import com.cobo.cold.scan.bean.ZxingConfig;
import com.cobo.cold.scan.bean.ZxingConfigBuilder;
import com.cobo.cold.scan.camera.CameraManager;
import com.cobo.cold.scan.view.PreviewFrame;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.viewmodel.ElectrumViewModel;
import com.cobo.cold.viewmodel.GlobalViewModel;
import com.cobo.cold.viewmodel.QrScanViewModel;
import com.cobo.cold.viewmodel.SharedDataViewModel;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import com.cobo.cold.viewmodel.UnknowQrCodeException;
import com.cobo.cold.viewmodel.UuidNotMatchException;
import com.cobo.cold.viewmodel.WatchWalletNotMatchException;
import com.cobo.cold.viewmodel.XpubNotMatchException;
import org.json.JSONException;
@ -59,6 +60,8 @@ import org.spongycastle.util.encoders.Hex;
import java.io.IOException;
import static com.cobo.cold.Utilities.IS_SETUP_VAULT;
import static com.cobo.cold.viewmodel.SupportedWatchWallet.ELECTRUM;
import static com.cobo.cold.viewmodel.SupportedWatchWallet.getSupportedWatchWallet;
public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
implements SurfaceHolder.Callback, Host {
@ -73,6 +76,7 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
private QrScanViewModel viewModel;
private ModalDialog dialog;
private SupportedWatchWallet watchWallet;
@Override
protected int setView() {
@ -81,6 +85,7 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
@Override
protected void init(View view) {
watchWallet = getSupportedWatchWallet(mActivity);
boolean isSetupVault = getArguments() != null && getArguments().getBoolean(IS_SETUP_VAULT);
purpose = getArguments() != null ? getArguments().getString("purpose") : "";
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
@ -98,7 +103,6 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
}
}
@Override
public void onResume() {
super.onResume();
@ -183,8 +187,16 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
navigateUp();
} else {
try {
if (tryParseElecturmTx(res) != null) {
handleElectrumTx(res);
if (tryParseElectrumTx(res) != null) {
if (SupportedWatchWallet.getSupportedWatchWallet(mActivity) == ELECTRUM) {
handleElectrumTx(res);
} else {
alert(getString(R.string.identification_failed),
getString(R.string.master_pubkey_not_match) +
getString(R.string.watch_wallet_not_match,
SupportedWatchWallet.getSupportedWatchWallet(mActivity)
.getWalletName(mActivity)));
}
} else {
alert(getString(R.string.unsupported_qrcode));
}
@ -202,7 +214,7 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
navigate(R.id.action_to_ElectrumTxConfirmFragment, bundle);
}
private ElectrumTx tryParseElecturmTx(String res) throws XpubNotMatchException {
private ElectrumTx tryParseElectrumTx(String res) throws XpubNotMatchException {
try {
byte[] data = Base43.decode(res);
ElectrumTx tx = ElectrumTx.parse(data);
@ -217,7 +229,7 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
}
private boolean checkElectrumExpub(ElectrumTx tx) {
String xpub = ViewModelProviders.of(mActivity).get(ElectrumViewModel.class).getXpub();
String xpub = ViewModelProviders.of(mActivity).get(GlobalViewModel.class).getXpub();
return tx.getInputs()
.stream()
.allMatch(input -> xpub.equals(input.pubKey.xpub));
@ -242,6 +254,12 @@ public class QRCodeScanFragment extends BaseFragment<QrcodeScanFragmentBinding>
} catch (UnknowQrCodeException e) {
e.printStackTrace();
alert(getString(R.string.unsupported_qrcode));
} catch (WatchWalletNotMatchException e) {
e.printStackTrace();
alert(getString(R.string.identification_failed),
getString(R.string.master_pubkey_not_match)
+ getString(R.string.watch_wallet_not_match,
SupportedWatchWallet.getSupportedWatchWallet(mActivity).getWalletName(mActivity)));
}
}

2
app/src/main/java/com/cobo/cold/ui/fragment/main/ReceiveCoinFragment.java

@ -28,6 +28,7 @@ import java.util.Objects;
import static com.cobo.cold.ui.fragment.Constants.KEY_ADDRESS;
import static com.cobo.cold.ui.fragment.Constants.KEY_ADDRESS_NAME;
import static com.cobo.cold.ui.fragment.Constants.KEY_ADDRESS_PATH;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_CODE;
public class ReceiveCoinFragment extends BaseFragment<ReceiveFragmentBinding> {
@ -44,6 +45,7 @@ public class ReceiveCoinFragment extends BaseFragment<ReceiveFragmentBinding> {
mBinding.setAddress(data.getString(KEY_ADDRESS));
mBinding.setAddressName(data.getString(KEY_ADDRESS_NAME));
mBinding.setCoinCode(data.getString(KEY_COIN_CODE));
mBinding.setPath(data.getString(KEY_ADDRESS_PATH));
mBinding.qrcode.setData(data.getString(KEY_ADDRESS));
}

3
app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java

@ -58,7 +58,7 @@ import java.util.Objects;
import static com.cobo.cold.ui.fragment.Constants.KEY_NAV_ID;
import static com.cobo.cold.ui.fragment.main.BroadcastTxFragment.KEY_TXID;
public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
public class TxConfirmFragment<T> extends BaseFragment<TxConfirmFragmentBinding> {
public static final String KEY_TX_DATA = "tx_data";
private final Runnable forgetPassword = () -> {
@ -245,6 +245,7 @@ public class TxConfirmFragment extends BaseFragment<TxConfirmFragmentBinding> {
private void subscribeSignState() {
viewModel.getSignState().observe(this, s -> {
if (TxConfirmViewModel.STATE_SIGNING.equals(s)) {
signingDialog = SigningDialog.newInstance();
signingDialog.show(mActivity.getSupportFragmentManager(), "");

80
app/src/main/java/com/cobo/cold/ui/fragment/main/TxListFragment.java

@ -22,10 +22,7 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.RecyclerView;
import com.cobo.cold.R;
@ -36,18 +33,16 @@ import com.cobo.cold.db.entity.TxEntity;
import com.cobo.cold.ui.common.FilterableBaseBindingAdapter;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.viewmodel.CoinListViewModel;
import org.json.JSONArray;
import org.json.JSONException;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import java.util.Comparator;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_CODE;
import static com.cobo.cold.ui.fragment.Constants.KEY_COIN_ID;
import static com.cobo.cold.ui.fragment.main.TxFragment.KEY_TX_ID;
import static com.cobo.cold.viewmodel.ElectrumViewModel.ELECTRUM_SIGN_ID;
import static com.cobo.cold.viewmodel.PsbtViewModel.WASABI_SIGN_ID;
public class TxListFragment extends BaseFragment<TxListBinding> {
@ -56,16 +51,6 @@ public class TxListFragment extends BaseFragment<TxListBinding> {
private String query;
private Comparator<TxEntity> txEntityComparator;
static Fragment newInstance(@NonNull String coinId, @NonNull String coinCode) {
TxListFragment fragment = new TxListFragment();
Bundle args = new Bundle();
args.putString(KEY_COIN_ID, coinId);
args.putString(KEY_COIN_CODE, coinCode);
fragment.setArguments(args);
return fragment;
}
@Override
protected int setView() {
return R.layout.tx_list;
@ -78,15 +63,16 @@ public class TxListFragment extends BaseFragment<TxListBinding> {
.get(CoinListViewModel.class);
adapter = new TxAdapter(mActivity);
mBinding.list.setAdapter(adapter);
mBinding.toolbar.setNavigationOnClickListener(v->navigateUp());
txCallback = tx -> {
Bundle bundle = new Bundle();
bundle.putString(KEY_TX_ID, tx.getTxId());
if (ELECTRUM_SIGN_ID.equals(tx.getSignId())) {
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_electrumTxFragment, bundle);
} else {
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_txFragment, bundle);
if (WASABI_SIGN_ID.equals(tx.getSignId())) {
navigate(R.id.action_to_psbtSignedTxFragment, bundle);
} else if(ELECTRUM_SIGN_ID.equals(tx.getSignId())){
navigate(R.id.action_to_electrumTxFragment, bundle);
}else {
navigate(R.id.action_to_txFragment, bundle);
}
};
@ -103,6 +89,7 @@ public class TxListFragment extends BaseFragment<TxListBinding> {
};
txEntities = txEntities.stream()
.filter(this::shouldShow)
.filter(this::filterByMode)
.sorted(txEntityComparator)
.collect(Collectors.toList());
adapter.setItems(txEntities);
@ -122,6 +109,21 @@ public class TxListFragment extends BaseFragment<TxListBinding> {
});
}
private boolean filterByMode(TxEntity txEntity) {
SupportedWatchWallet watchWallet = SupportedWatchWallet.getSupportedWatchWallet(mActivity);
switch (watchWallet) {
case COBO:
return !txEntity.getSignId().endsWith("_sign_id");
case ELECTRUM:
return ELECTRUM_SIGN_ID.equals(txEntity.getSignId());
case WASABI:
return WASABI_SIGN_ID.equals(txEntity.getSignId());
case BLUE:
return ELECTRUM_SIGN_ID.equals(txEntity.getSignId());
}
return false;
}
private boolean shouldShow(TxEntity tx) {
return Utilities.getCurrentBelongTo(mActivity).equals(tx.getBelongTo());
}
@ -152,38 +154,6 @@ public class TxListFragment extends BaseFragment<TxListBinding> {
protected void onBindItem(TxListItemBinding binding, TxEntity item) {
binding.setTx(item);
binding.setTxCallback(txCallback);
if (ELECTRUM_SIGN_ID.equals(item.getSignId())) {
binding.fromWallet.setText(R.string.from_electrum);
binding.fromWallet.setVisibility(View.VISIBLE);
binding.amount.setVisibility(View.GONE);
} else {
binding.fromWallet.setVisibility(View.GONE);
}
updateFrom(binding, item);
updateTo(binding, item);
}
private void updateTo(TxListItemBinding binding, TxEntity item) {
String to = item.getTo();
binding.to.setText(item.getTo());
try {
JSONArray outputs = new JSONArray(to);
binding.to.setText(outputs.getJSONObject(0).getString("address"));
} catch (JSONException e) {
e.printStackTrace();
}
}
private void updateFrom(TxListItemBinding binding, TxEntity item) {
String from = item.getFrom();
binding.from.setText(item.getFrom());
try {
JSONArray inputs = new JSONArray(from);
String address = inputs.getJSONObject(0).getString("address");
binding.from.setText(address);
} catch (JSONException e) {
e.printStackTrace();
}
}
}

97
app/src/main/java/com/cobo/cold/ui/fragment/main/WalletInfoFragment.java

@ -0,0 +1,97 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.R;
import com.cobo.cold.databinding.WalletInfoBinding;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.viewmodel.GlobalViewModel;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import com.cobo.cold.viewmodel.WalletInfoViewModel;
import static com.cobo.cold.ui.fragment.Constants.KEY_TITLE;
public class WalletInfoFragment extends BaseFragment<WalletInfoBinding> {
private Coins.Account account;
@Override
protected int setView() {
return R.layout.wallet_info;
}
@Override
protected void init(View view) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.switchAddress.setOnClickListener(v -> switchAddressFormat());
account = GlobalViewModel.getAccount(mActivity);
SupportedWatchWallet watchOnly = SupportedWatchWallet.getSupportedWatchWallet(mActivity);
if (watchOnly != SupportedWatchWallet.ELECTRUM
&& watchOnly != SupportedWatchWallet.GENERIC) {
mBinding.switchAddress.setVisibility(View.GONE);
}
mBinding.addressFormat.setText(getAddressFormat());
mBinding.addressType.setText(account.getType());
mBinding.path.setText(account.getPath());
WalletInfoViewModel viewModel = ViewModelProviders.of(this)
.get(WalletInfoViewModel.class);
viewModel.getFingerprint().observe(this, s -> {
if (!TextUtils.isEmpty(s)) {
mBinding.fingerprint.setText(s);
}
});
viewModel.getXpub(account).observe(this, s -> {
if (!TextUtils.isEmpty(s)) {
mBinding.xpub.setText(s);
}
});
}
private String getAddressFormat() {
switch (account) {
case SegWit:
return getString(R.string.native_segwit);
case P2PKH:
return getString(R.string.p2pkh);
case P2SH:
return getString(R.string.nested_segwit);
}
return "";
}
private void switchAddressFormat() {
Bundle data = new Bundle();
data.putInt(KEY_TITLE, R.string.select_address_format);
navigate(R.id.action_to_selectAddressFormatFragment, data);
}
@Override
protected void initData(Bundle savedInstanceState) {
}
}

2
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumBroadcastTxFragment.java

@ -39,7 +39,7 @@ import org.spongycastle.util.encoders.Hex;
import java.util.Objects;
import static com.cobo.cold.ui.fragment.main.electrum.ElectrumTxConfirmFragment.showExportTxnDialog;
import static com.cobo.cold.ui.fragment.main.electrum.UnsignedTxFragment.showExportTxnDialog;
public class ElectrumBroadcastTxFragment extends BaseFragment<BroadcastElectrumTxFragmentBinding> {

50
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumExportFragment.java

@ -26,23 +26,25 @@ import android.view.View;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.R;
import com.cobo.cold.databinding.CommonModalBinding;
import com.cobo.cold.databinding.ElectrumExportBinding;
import com.cobo.cold.databinding.ExportSdcardModalBinding;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.SetupVaultActivity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.Storage;
import com.cobo.cold.viewmodel.ElectrumViewModel;
import com.cobo.cold.viewmodel.GlobalViewModel;
import static com.cobo.cold.viewmodel.GlobalViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.GlobalViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.GlobalViewModel.writeToSdcard;
import static com.cobo.cold.viewmodel.ElectrumViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.ElectrumViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.ElectrumViewModel.writeToSdcard;
public class ElectrumExportFragment extends BaseFragment<ElectrumExportBinding> {
private static final String EXTEND_PUB_FILE_NAME = "p2wpkh-p2sh-pubkey.txt";
private String exPub;
@Override
@ -53,8 +55,13 @@ public class ElectrumExportFragment extends BaseFragment<ElectrumExportBinding>
@Override
protected void init(View view) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
ElectrumViewModel viewModel = ViewModelProviders.of(mActivity).get(ElectrumViewModel.class);
viewModel.getMasterPublicKey().observe(this, s -> {
GlobalViewModel viewModel;
if (mActivity instanceof SetupVaultActivity) {
viewModel = ViewModelProviders.of(this).get(GlobalViewModel.class);
} else {
viewModel = ViewModelProviders.of(mActivity).get(GlobalViewModel.class);
}
viewModel.getExtendPublicKey().observe(this, s -> {
if (!TextUtils.isEmpty(s)) {
exPub = s;
mBinding.qrcode.setData(s);
@ -62,9 +69,15 @@ public class ElectrumExportFragment extends BaseFragment<ElectrumExportBinding>
}
});
mBinding.info.setOnClickListener(v -> showElectrumInfo());
mBinding.done.setOnClickListener(v->{
MainActivity activity = (MainActivity) mActivity;
activity.getNavController().popBackStack(R.id.assetFragment,false);
mBinding.addressType.setText(getString(R.string.master_xpub,
GlobalViewModel.getAddressFormat(mActivity)));
mBinding.done.setOnClickListener(v -> {
if (mActivity instanceof SetupVaultActivity) {
navigate(R.id.action_to_setupCompleteFragment);
} else {
MainActivity activity = (MainActivity) mActivity;
activity.getNavController().popBackStack(R.id.assetFragment, false);
}
});
mBinding.exportToSdcard.setOnClickListener(v -> {
Storage storage = Storage.createByEnvironment(mActivity);
@ -75,12 +88,12 @@ public class ElectrumExportFragment extends BaseFragment<ElectrumExportBinding>
ExportSdcardModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.export_sdcard_modal, null, false);
binding.title.setText(R.string.export_xpub_text_file);
binding.fileName.setText(EXTEND_PUB_FILE_NAME);
binding.fileName.setText(getFileName());
binding.actionHint.setText(R.string.electrum_import_xpub_action);
binding.cancel.setOnClickListener(vv -> modalDialog.dismiss());
binding.confirm.setOnClickListener(vv -> {
modalDialog.dismiss();
if (writeToSdcard(storage, exPub, EXTEND_PUB_FILE_NAME)) {
if (writeToSdcard(storage, exPub, getFileName())) {
exportSuccess(mActivity, null);
}
});
@ -105,6 +118,19 @@ public class ElectrumExportFragment extends BaseFragment<ElectrumExportBinding>
modalDialog.show(mActivity.getSupportFragmentManager(), "");
}
private String getFileName() {
Coins.Account account = GlobalViewModel.getAccount(mActivity);
switch (account) {
case SegWit:
return "p2wpkh-pubkey.txt";
case P2SH:
return "p2wpkh-p2sh-pubkey.txt";
case P2PKH:
return "p2pkh-pubkey.txt";
}
return "p2wpkh-p2sh-pubkey.txt";
}
@Override
protected void initData(Bundle savedInstanceState) {

14
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxnListFragment.java

@ -21,7 +21,6 @@ import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProviders;
@ -29,8 +28,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ElectrumTxnBinding;
import com.cobo.cold.databinding.TxnListBinding;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.databinding.FileListBinding;
import com.cobo.cold.ui.common.BaseBindingAdapter;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ModalDialog;
@ -38,9 +36,9 @@ import com.cobo.cold.viewmodel.ElectrumViewModel;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.cobo.cold.viewmodel.ElectrumViewModel.hasSdcard;
import static com.cobo.cold.viewmodel.GlobalViewModel.hasSdcard;
public class ElectrumTxnListFragment extends BaseFragment<TxnListBinding>
public class ElectrumTxnListFragment extends BaseFragment<FileListBinding>
implements Callback {
public static final String TAG = "ElectrumTxnListFragment";
@ -50,14 +48,12 @@ public class ElectrumTxnListFragment extends BaseFragment<TxnListBinding>
@Override
protected int setView() {
return R.layout.txn_list;
return R.layout.file_list;
}
@Override
protected void init(View view) {
mActivity.setSupportActionBar(mBinding.toolbar);
mBinding.toolbar.setNavigationOnClickListener(((MainActivity) mActivity)::toggleDrawer);
mBinding.toolbar.setTitle("");
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
viewModel = ViewModelProviders.of(mActivity).get(ElectrumViewModel.class);
adapter = new TxnAdapter(mActivity, this);
initViews();

177
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ExportXpubGuideFragment.java

@ -0,0 +1,177 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.main.electrum;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.databinding.DataBindingUtil;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ExportSdcardModalBinding;
import com.cobo.cold.databinding.ExportXpubGuideBinding;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.SetupVaultActivity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.Storage;
import com.cobo.cold.viewmodel.GlobalViewModel;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import org.json.JSONObject;
import static com.cobo.cold.viewmodel.GlobalViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.GlobalViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.GlobalViewModel.writeToSdcard;
public class ExportXpubGuideFragment extends BaseFragment<ExportXpubGuideBinding> {
private SupportedWatchWallet watchWallet;
private JSONObject wasabiXpubJson;
private static final String WASABI_XPUB_FILENAME = "CoboVault-Wasabi.json";
@Override
protected int setView() {
return R.layout.export_xpub_guide;
}
@Override
protected void init(View view) {
watchWallet = SupportedWatchWallet.getSupportedWatchWallet(mActivity);
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.toolbarTitle.setText(getTitle());
mBinding.export.setOnClickListener(v -> export());
if (mActivity instanceof MainActivity) {
mBinding.skip.setVisibility(View.GONE);
} else {
mBinding.skip.setOnClickListener(v -> navigate(R.id.action_to_setupCompleteFragment));
}
mBinding.text1.setText(getText1());
mBinding.text2.setText(getText2());
mBinding.export.setText(getButtonText());
wasabiXpubJson = GlobalViewModel.getXpubInfo(mActivity);
}
private void export() {
switch (watchWallet) {
case ELECTRUM:
navigate(R.id.export_electrum_ypub);
break;
case COBO:
navigate(R.id.export_xpub_cobo);
break;
case WASABI:
exportXpub();
break;
case BLUE:
//return R.string.show_qrcode;
break;
case GENERIC:
//navigate(R.id.export_electrum_ypub);
break;
}
}
public void exportXpub() {
Storage storage = Storage.createByEnvironment(mActivity);
if (storage == null || storage.getExternalDir() == null) {
showNoSdcardModal(mActivity);
} else {
ModalDialog modalDialog = ModalDialog.newInstance();
ExportSdcardModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.export_sdcard_modal, null, false);
binding.title.setText(R.string.export_xpub_text_file);
binding.fileName.setText(WASABI_XPUB_FILENAME);
binding.actionHint.setVisibility(View.GONE);
binding.cancel.setOnClickListener(vv -> modalDialog.dismiss());
binding.confirm.setOnClickListener(vv -> {
modalDialog.dismiss();
if (writeToSdcard(storage, wasabiXpubJson.toString(), WASABI_XPUB_FILENAME)) {
Runnable runnable = null;
if (mActivity instanceof SetupVaultActivity) {
runnable = () -> navigate(R.id.action_to_setupCompleteFragment);
}
exportSuccess(mActivity, runnable);
}
});
modalDialog.setBinding(binding);
modalDialog.show(mActivity.getSupportFragmentManager(), "");
}
}
private int getButtonText() {
switch (watchWallet) {
case ELECTRUM:
return R.string.show_master_public_key_qrcode;
case WASABI:
return R.string.export_wallet;
case COBO:
case BLUE:
return R.string.show_qrcode;
}
return 0;
}
private int getTitle() {
switch (watchWallet) {
case ELECTRUM:
return R.string.export_xpub_guide_title_electrum;
case WASABI:
return R.string.export_xpub_guide_title_wasabi;
case COBO:
return R.string.export_xpub_guide_title_cobo;
case BLUE:
return R.string.export_xpub_guide_title_blue;
}
return 0;
}
private int getText1() {
switch (watchWallet) {
case ELECTRUM:
return R.string.export_xpub_guide_text1_electrum;
case WASABI:
return R.string.export_xpub_guide_text1_wasabi;
case COBO:
return R.string.export_xpub_guide_text1_cobo;
case BLUE:
return R.string.export_xpub_guide_text1_blue;
}
return 0;
}
private int getText2() {
switch (watchWallet) {
case ELECTRUM:
return R.string.export_xpub_guide_text2_electrum;
case WASABI:
return R.string.export_xpub_guide_text2_wasabi;
case COBO:
return R.string.export_xpub_guide_text2_cobo;
case BLUE:
return R.string.export_xpub_guide_text2_blue;
}
return 0;
}
@Override
protected void initData(Bundle savedInstanceState) {
}
}

59
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxFragment.java → app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/SignedTxFragment.java

@ -29,13 +29,14 @@ import androidx.lifecycle.ViewModelProviders;
import com.cobo.coinlib.utils.Base43;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.R;
import com.cobo.cold.databinding.ElectrumTxBinding;
import com.cobo.cold.databinding.SignedTxBinding;
import com.cobo.cold.db.entity.TxEntity;
import com.cobo.cold.ui.fragment.BaseFragment;
import com.cobo.cold.ui.fragment.main.TransactionItem;
import com.cobo.cold.ui.fragment.main.TransactionItemAdapter;
import com.cobo.cold.viewmodel.CoinListViewModel;
import com.cobo.cold.viewmodel.ElectrumViewModel;
import com.cobo.cold.viewmodel.GlobalViewModel;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import org.json.JSONArray;
import org.json.JSONException;
@ -47,51 +48,46 @@ import java.util.List;
import java.util.Objects;
import static com.cobo.cold.ui.fragment.main.electrum.ElectrumBroadcastTxFragment.showElectrumInfo;
import static com.cobo.cold.ui.fragment.main.electrum.ElectrumTxConfirmFragment.showExportTxnDialog;
import static com.cobo.cold.ui.fragment.main.electrum.UnsignedTxFragment.showExportTxnDialog;
public class ElectrumTxFragment extends BaseFragment<ElectrumTxBinding> {
public class SignedTxFragment extends BaseFragment<SignedTxBinding> {
public static final String KEY_TX_ID = "txid";
private TxEntity txEntity;
private static final String KEY_TX_ID = "txid";
protected TxEntity txEntity;
private List<String> changeAddress = new ArrayList<>();
@Override
protected int setView() {
return R.layout.electrum_tx;
return R.layout.signed_tx;
}
@Override
protected void init(View view) {
Bundle data = Objects.requireNonNull(getArguments());
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
String walletName = SupportedWatchWallet.getSupportedWatchWallet(mActivity)
.getWalletName(mActivity);
mBinding.txDetail.watchWallet.setText(walletName);
ViewModelProviders.of(mActivity)
.get(GlobalViewModel.class)
.getChangeAddress()
.observe(this, address -> this.changeAddress = address);
CoinListViewModel viewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class);
viewModel.loadTx(data.getString(KEY_TX_ID)).observe(this, txEntity -> {
mBinding.setTx(txEntity);
this.txEntity = txEntity;
String signTx = getSignTxString(txEntity);
if (signTx.length() <= 1000) {
new Handler().postDelayed(() -> mBinding.txDetail.qrcodeLayout.qrcode.setData(signTx), 500);
mBinding.txDetail.export.setVisibility(View.GONE);
mBinding.txDetail.exportToSdcardHint.setOnClickListener(v ->
showExportTxnDialog(mActivity, txEntity.getTxId(), txEntity.getSignedHex(), null));
mBinding.txDetail.info.setOnClickListener(v -> showElectrumInfo(mActivity));
} else {
mBinding.txDetail.qr.setVisibility(View.GONE);
}
displaySignResult(txEntity);
refreshAmount();
refreshFromList();
refreshReceiveList();
mBinding.txDetail.exportToSdcard.setOnClickListener(v -> {
showExportTxnDialog(mActivity, txEntity.getTxId(), txEntity.getSignedHex(),null);
});
mBinding.txDetail.exportToSdcard.setOnClickListener(v -> showExportDialog());
});
}
ViewModelProviders.of(mActivity)
.get(ElectrumViewModel.class)
.getChangeAddress()
.observe(this, address -> this.changeAddress = address);
protected void showExportDialog() {
showExportTxnDialog(mActivity, txEntity.getTxId(), txEntity.getSignedHex(),null);
}
private void refreshFromList() {
@ -150,9 +146,18 @@ public class ElectrumTxFragment extends BaseFragment<ElectrumTxBinding> {
}
private String getSignTxString(TxEntity txEntity) {
protected void displaySignResult(TxEntity txEntity) {
byte[] txData = Hex.decode(txEntity.getSignedHex());
return Base43.encode(txData);
String base43 = Base43.encode(txData);
if (base43.length() <= 1000) {
new Handler().postDelayed(() -> mBinding.txDetail.qrcodeLayout.qrcode.setData(base43), 500);
mBinding.txDetail.export.setVisibility(View.GONE);
mBinding.txDetail.exportToSdcardHint.setOnClickListener(v ->
showExportTxnDialog(mActivity, txEntity.getTxId(), txEntity.getSignedHex(), null));
mBinding.txDetail.info.setOnClickListener(v -> showElectrumInfo(mActivity));
} else {
mBinding.txDetail.qr.setVisibility(View.GONE);
}
}
}

97
app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/ElectrumTxConfirmFragment.java → app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/UnsignedTxFragment.java

@ -50,8 +50,10 @@ import com.cobo.cold.ui.modal.SigningDialog;
import com.cobo.cold.ui.views.AuthenticateModal;
import com.cobo.cold.update.utils.Storage;
import com.cobo.cold.util.KeyStoreUtil;
import com.cobo.cold.viewmodel.ElectrumViewModel;
import com.cobo.cold.viewmodel.GlobalViewModel;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import com.cobo.cold.viewmodel.TxConfirmViewModel;
import com.cobo.cold.viewmodel.WatchWalletNotMatchException;
import com.cobo.cold.viewmodel.XpubNotMatchException;
import org.json.JSONArray;
@ -66,13 +68,13 @@ import java.util.Objects;
import static com.cobo.cold.ui.fragment.Constants.KEY_NAV_ID;
import static com.cobo.cold.ui.fragment.main.BroadcastTxFragment.KEY_TXID;
import static com.cobo.cold.viewmodel.ElectrumViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.ElectrumViewModel.hasSdcard;
import static com.cobo.cold.viewmodel.ElectrumViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.ElectrumViewModel.writeToSdcard;
import static com.cobo.cold.viewmodel.GlobalViewModel.exportSuccess;
import static com.cobo.cold.viewmodel.GlobalViewModel.hasSdcard;
import static com.cobo.cold.viewmodel.GlobalViewModel.showNoSdcardModal;
import static com.cobo.cold.viewmodel.GlobalViewModel.writeToSdcard;
import static com.cobo.cold.viewmodel.TxConfirmViewModel.STATE_NONE;
public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFragmentBinding> {
public class UnsignedTxFragment extends BaseFragment<ElectrumTxConfirmFragmentBinding> {
private final Runnable forgetPassword = () -> {
Bundle data = new Bundle();
@ -80,15 +82,14 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_verifyMnemonic, data);
};
private TxConfirmViewModel viewModel;
protected TxConfirmViewModel viewModel;
private SigningDialog signingDialog;
private TxEntity txEntity;
private ModalDialog addingAddressDialog;
private String txnData;
private List<String> changeAddress = new ArrayList<>();
public static void showExportTxnDialog(AppCompatActivity activity, String txId, String hex,
Runnable onExportSuccess) {
static void showExportTxnDialog(AppCompatActivity activity, String txId, String hex,
Runnable onExportSuccess) {
ModalDialog modalDialog = ModalDialog.newInstance();
ExportSdcardModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(activity),
R.layout.export_sdcard_modal, null, false);
@ -101,7 +102,8 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
modalDialog.dismiss();
if (hasSdcard(activity)) {
Storage storage = Storage.createByEnvironment(activity);
boolean result = writeToSdcard(storage, generateElectrumTxn(hex), fileName);
boolean result = writeToSdcard(Objects.requireNonNull(storage),
generateElectrumTxn(hex), fileName);
if (result) {
exportSuccess(activity, onExportSuccess);
}
@ -134,20 +136,23 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
@Override
protected void init(View view) {
Bundle bundle = Objects.requireNonNull(getArguments());
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.txDetail.txIdInfo.setVisibility(View.GONE);
mBinding.txDetail.export.setVisibility(View.GONE);
mBinding.txDetail.qr.setVisibility(View.GONE);
txnData = bundle.getString("txn");
String walletName = SupportedWatchWallet.getSupportedWatchWallet(mActivity)
.getWalletName(mActivity);
mBinding.txDetail.watchWallet.setText(walletName);
viewModel = ViewModelProviders.of(this).get(TxConfirmViewModel.class);
mBinding.setViewModel(viewModel);
subscribeTxEntityState();
mBinding.sign.setOnClickListener(v -> handleSign());
ViewModelProviders.of(mActivity)
.get(ElectrumViewModel.class)
.get(GlobalViewModel.class)
.getChangeAddress()
.observe(this, address -> this.changeAddress = address);
subscribeTxEntityState();
mBinding.sign.setOnClickListener(v -> handleSign());
}
private void handleSign() {
@ -179,7 +184,7 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
}
}
private AuthenticateModal.OnVerify signWithVerifyInfo() {
protected AuthenticateModal.OnVerify signWithVerifyInfo() {
return token -> {
viewModel.setToken(token);
viewModel.handleSign();
@ -187,44 +192,40 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
};
}
protected void parseTx() {
String txnData = Objects.requireNonNull(getArguments()).getString("txn");
viewModel.parseTxnData(txnData);
}
private void subscribeTxEntityState() {
ProgressModalDialog dialog = new ProgressModalDialog();
dialog.show(mActivity.getSupportFragmentManager(), "");
viewModel.parseTxnData(txnData);
parseTx();
viewModel.getObservableTx().observe(this, txEntity -> {
if (txEntity != null) {
dialog.dismiss();
this.txEntity = txEntity;
mBinding.setTx(txEntity);
refreshAmount();
refreshFromList();
refreshReceiveList();
refreshUI();
}
});
observeParseTx(dialog);
}
viewModel.getAddingAddressState().observe(this, b -> {
if (b) {
addingAddressDialog = ModalDialog.newInstance();
ProgressModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.progress_modal, null, false);
binding.text.setText(R.string.sync_in_progress);
binding.text.setVisibility(View.VISIBLE);
addingAddressDialog.setBinding(binding);
addingAddressDialog.show(mActivity.getSupportFragmentManager(), "");
} else {
if (addingAddressDialog != null) {
addingAddressDialog.dismiss();
}
}
});
private void refreshUI() {
refreshAmount();
refreshFromList();
refreshReceiveList();
}
private void observeParseTx(ProgressModalDialog dialog) {
viewModel.parseTxException().observe(this, ex -> {
if (ex != null) {
ex.printStackTrace();
dialog.dismiss();
String errorMessage = getString(R.string.incorrect_tx_data);
if (ex instanceof XpubNotMatchException) {
if (ex instanceof XpubNotMatchException || ex instanceof WatchWalletNotMatchException) {
errorMessage = getString(R.string.master_pubkey_not_match);
}
ModalDialog.showCommonModal(mActivity,
@ -237,6 +238,24 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
});
}
private void observeAddAddress() {
viewModel.getAddingAddressState().observe(this, b -> {
if (b) {
addingAddressDialog = ModalDialog.newInstance();
ProgressModalBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mActivity),
R.layout.progress_modal, null, false);
binding.text.setText(R.string.sync_in_progress);
binding.text.setVisibility(View.VISIBLE);
addingAddressDialog.setBinding(binding);
addingAddressDialog.show(mActivity.getSupportFragmentManager(), "");
} else {
if (addingAddressDialog != null) {
addingAddressDialog.dismiss();
}
}
});
}
private void refreshAmount() {
SpannableStringBuilder style = new SpannableStringBuilder(txEntity.getAmount());
style.setSpan(new ForegroundColorSpan(mActivity.getColor(R.color.colorAccent)),
@ -291,7 +310,7 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
mBinding.txDetail.fromList.setAdapter(adapter);
}
private void subscribeSignState() {
protected void subscribeSignState() {
viewModel.getSignState().observe(this, s -> {
if (TxConfirmViewModel.STATE_SIGNING.equals(s)) {
signingDialog = SigningDialog.newInstance();
@ -326,7 +345,7 @@ public class ElectrumTxConfirmFragment extends BaseFragment<ElectrumTxConfirmFra
});
}
private void onSignSuccess() {
protected void onSignSuccess() {
handleTxnSignSuccess();
viewModel.getSignState().removeObservers(this);
}

99
app/src/main/java/com/cobo/cold/ui/fragment/setting/ChooseWatchWalletFragment.java

@ -0,0 +1,99 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.setting;
import android.os.Bundle;
import android.view.View;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.R;
import com.cobo.cold.Utilities;
import com.cobo.cold.ui.SetupVaultActivity;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import java.util.Arrays;
import static com.cobo.cold.ui.fragment.Constants.KEY_TITLE;
import static com.cobo.cold.ui.fragment.setting.MainPreferenceFragment.SETTING_ADDRESS_FORMAT;
import static com.cobo.cold.ui.fragment.setting.MainPreferenceFragment.SETTING_CHOOSE_WATCH_WALLET;
public class ChooseWatchWalletFragment extends ListPreferenceFragment {
@Override
protected void init(View view) {
Bundle data = getArguments();
if (data != null) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.toolbarTitle.setText(data.getInt(KEY_TITLE));
} else {
mBinding.toolbar.setVisibility(View.GONE);
}
prefs = Utilities.getPrefs(mActivity);
entries = getResources().getStringArray(getEntries());
values = getResources().getStringArray(getValues());
value = prefs.getString(getKey(), defaultValue());
adapter = new Adapter(mActivity);
if (mActivity instanceof SetupVaultActivity) {
adapter.setItems(Arrays.asList(Arrays.copyOfRange(entries,0,entries.length - 1)));
} else {
adapter.setItems(Arrays.asList(entries));
}
mBinding.list.setAdapter(adapter);
}
@Override
protected int getEntries() {
return R.array.watch_wallet_list;
}
@Override
protected int getValues() {
return R.array.watch_wallet_list_values;
}
@Override
protected String getKey() {
return SETTING_CHOOSE_WATCH_WALLET;
}
@Override
protected String defaultValue() {
return SupportedWatchWallet.ELECTRUM.getWalletId();
}
@Override
public void onSelect(int position) {
String old = value;
value = values[position].toString();
if (!old.equals(value)) {
setWatchWallet();
adapter.notifyDataSetChanged();
}
}
private void setWatchWallet() {
prefs.edit().putString(SETTING_CHOOSE_WATCH_WALLET, value).apply();
if (value.equals(SupportedWatchWallet.COBO.getWalletId())) {
prefs.edit().putString(SETTING_ADDRESS_FORMAT, Coins.Account.P2SH.getType()).apply();
} else if (value.equals(SupportedWatchWallet.WASABI.getWalletId())
|| value.equals(SupportedWatchWallet.BLUE.getWalletId())) {
prefs.edit().putString(SETTING_ADDRESS_FORMAT, Coins.Account.SegWit.getType()).apply();
}
}
}

5
app/src/main/java/com/cobo/cold/ui/fragment/setting/FingerprintEnrollFragment.java

@ -19,6 +19,8 @@ package com.cobo.cold.ui.fragment.setting;
import android.hardware.fingerprint.Fingerprint;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@ -231,6 +233,9 @@ public class FingerprintEnrollFragment extends BaseFragment<FingerprintEnrollBin
binding.title.setText(R.string.fingerprint_rename_subtitle);
binding.setInput(input);
binding.inputBox.setSelectAllOnFocus(true);
binding.inputBox.setFilters(new InputFilter[]{new InputFilter.LengthFilter(30)});
binding.inputBox.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
binding.inputBox.setTransformationMethod(null);
binding.close.setOnClickListener(v -> dialog.dismiss());
binding.confirm.setOnClickListener(v -> {
fingerprintKit.renameFingerprint(currentEnrolled, Objects.requireNonNull(input.get()));

3
app/src/main/java/com/cobo/cold/ui/fragment/setting/FingerprintManageFragment.java

@ -21,6 +21,7 @@ import android.graphics.Color;
import android.hardware.fingerprint.Fingerprint;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.InputType;
import android.view.LayoutInflater;
import android.view.View;
@ -115,6 +116,8 @@ public class FingerprintManageFragment extends BaseFragment<FingerprintManageBin
binding.setInput(input);
binding.inputBox.setSelectAllOnFocus(true);
binding.inputBox.setFilters(new InputFilter[]{new InputFilter.LengthFilter(30)});
binding.inputBox.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
binding.inputBox.setTransformationMethod(null);
binding.close.setOnClickListener(v -> dialog.dismiss());
binding.confirm.setOnClickListener(v -> {
fpKit.renameFingerprint(fingerprint, input.get());

24
app/src/main/java/com/cobo/cold/ui/fragment/setting/ListPreferenceFragment.java

@ -34,18 +34,18 @@ import com.cobo.cold.ui.common.BaseBindingAdapter;
import com.cobo.cold.ui.fragment.BaseFragment;
import java.util.Arrays;
import java.util.Objects;
import static com.cobo.cold.ui.fragment.Constants.KEY_TITLE;
public abstract class ListPreferenceFragment extends BaseFragment<ListPreferenceBinding>
implements ListPreferenceCallback {
public abstract class ListPreferenceFragment
extends BaseFragment<ListPreferenceBinding> implements ListPreferenceCallback {
protected Adapter adapter;
protected SharedPreferences prefs;
protected CharSequence[] values;
protected String value;
private CharSequence[] entries;
protected CharSequence[] entries;
protected CharSequence[] subTitles;
@Override
protected int setView() {
@ -62,9 +62,13 @@ public abstract class ListPreferenceFragment extends BaseFragment<ListPreference
@Override
protected void init(View view) {
Objects.requireNonNull(getArguments());
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.toolbarTitle.setText(getArguments().getInt(KEY_TITLE));
Bundle data = getArguments();
if (data != null) {
mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp());
mBinding.toolbarTitle.setText(data.getInt(KEY_TITLE));
} else {
mBinding.toolbar.setVisibility(View.GONE);
}
prefs = Utilities.getPrefs(mActivity);
entries = getResources().getStringArray(getEntries());
values = getResources().getStringArray(getValues());
@ -94,6 +98,12 @@ public abstract class ListPreferenceFragment extends BaseFragment<ListPreference
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SettingItemSelectableBinding binding = DataBindingUtil.getBinding(holder.itemView);
binding.title.setText(entries[position]);
if (subTitles == null) {
binding.subTitle.setVisibility(View.GONE);
} else {
binding.subTitle.setVisibility(View.VISIBLE);
binding.subTitle.setText(subTitles[position]);
}
binding.setIndex(position);
binding.setCallback(ListPreferenceFragment.this);
if (values[position].equals(value)) {

16
app/src/main/java/com/cobo/cold/ui/fragment/setting/MainPreferenceFragment.java

@ -47,6 +47,7 @@ import com.cobo.cold.databinding.CommonModalBinding;
import com.cobo.cold.fingerprint.FingerprintKit;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.SetupVaultActivity;
import com.cobo.cold.ui.fragment.Constants;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.ui.modal.ProgressModalDialog;
import com.cobo.cold.ui.preference.SimplePreference;
@ -55,6 +56,7 @@ import com.cobo.cold.ui.views.AuthenticateModal;
import com.cobo.cold.ui.views.UpdatingHelper;
import com.cobo.cold.update.data.UpdateManifest;
import com.cobo.cold.util.DataCleaner;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import com.cobo.cold.viewmodel.UpdatingViewModel;
import java.util.List;
@ -83,6 +85,8 @@ public class MainPreferenceFragment extends PreferenceFragmentCompat {
private static final String SETTING_VERSION = "setting_version";
private static final String SETTING_FINGERPRINT = "setting_fingerprint";
private static final String SETTING_PASSPHRASE = "setting_passphrase";
public static final String SETTING_CHOOSE_WATCH_WALLET = "setting_choose_watch_only_wallet";
public static final String SETTING_ADDRESS_FORMAT = "setting_address_format";
private SwitchPreference switchPreference;
private SimplePreference versionPreference;
@ -98,7 +102,6 @@ public class MainPreferenceFragment extends PreferenceFragmentCompat {
protected AppCompatActivity mActivity;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@ -138,7 +141,12 @@ public class MainPreferenceFragment extends PreferenceFragmentCompat {
switchPreference = findPreference(SETTING_PATTERN_UNLOCK);
if (switchPreference != null) {
switchPreference.setChecked(Utilities.isPatternUnlock(mActivity));
}
SimplePreference chooseWalletPreference = findPreference(SETTING_CHOOSE_WATCH_WALLET);
if (chooseWalletPreference != null) {
chooseWalletPreference.setRemindText(SupportedWatchWallet.getSupportedWatchWallet(mActivity)
.getWalletName(mActivity));
}
Looper.getMainLooper().getQueue().addIdleHandler(() -> {
@ -262,6 +270,12 @@ public class MainPreferenceFragment extends PreferenceFragmentCompat {
update();
}
break;
case SETTING_CHOOSE_WATCH_WALLET:
Bundle data = new Bundle();
data.putInt(Constants.KEY_TITLE, R.string.setting_language);
Navigation.findNavController(Objects.requireNonNull(getView()))
.navigate(R.id.action_to_chooseWatchOnly, data);
break;
default:
break;
}

76
app/src/main/java/com/cobo/cold/ui/fragment/setup/SelectAddressFormatFragment.java

@ -0,0 +1,76 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.setup;
import android.view.View;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.R;
import com.cobo.cold.ui.MainActivity;
import com.cobo.cold.ui.fragment.setting.ListPreferenceFragment;
import static com.cobo.cold.ui.fragment.setting.MainPreferenceFragment.SETTING_ADDRESS_FORMAT;
public class SelectAddressFormatFragment extends ListPreferenceFragment {
@Override
protected void init(View view) {
super.init(view);
subTitles = getResources().getStringArray(R.array.address_format_subtitle);
mBinding.confirm.setVisibility(View.VISIBLE);
mBinding.confirm.setText(R.string.next);
mBinding.confirm.setOnClickListener(v -> next());
if (mActivity instanceof MainActivity) {
mBinding.confirm.setVisibility(View.GONE);
}
}
private void next() {
navigate(R.id.action_to_export_xpub_guide);
}
@Override
protected int getEntries() {
return R.array.address_format;
}
@Override
protected int getValues() {
return R.array.address_format_value;
}
@Override
protected String getKey() {
return SETTING_ADDRESS_FORMAT;
}
@Override
protected String defaultValue() {
return Coins.Account.P2SH.getType();
}
@Override
public void onSelect(int position) {
String old = value;
value = values[position].toString();
if (!old.equals(value)) {
prefs.edit().putString(getKey(), value).apply();
adapter.notifyDataSetChanged();
}
}
}

65
app/src/main/java/com/cobo/cold/ui/fragment/setup/SetupSyncFragment.java

@ -1,65 +0,0 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.setup;
import android.text.TextUtils;
import android.view.View;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProviders;
import com.cobo.cold.R;
import com.cobo.cold.databinding.SetupSyncBinding;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.viewmodel.CoinListViewModel;
import com.cobo.cold.viewmodel.SetupVaultViewModel;
import java.util.List;
public class SetupSyncFragment extends SetupVaultBaseFragment<SetupSyncBinding> {
@Override
protected int setView() {
return R.layout.setup_sync;
}
@Override
protected void init(View view) {
super.init(view);
mBinding.complete.setOnClickListener(this::complete);
SetupVaultViewModel model = ViewModelProviders.of(mActivity).get(SetupVaultViewModel.class);
subscribe(model.getCoins());
}
private void complete(View view) {
navigate(R.id.action_to_setupCompleteFragment);
}
private void subscribe(LiveData<List<CoinEntity>> coins) {
coins.observe(this, this::generateSyncData);
}
private void generateSyncData(List<CoinEntity> coinEntities) {
ViewModelProviders.of(mActivity).get(CoinListViewModel.class)
.generateSync(coinEntities).observe(this, sync -> {
if (!TextUtils.isEmpty(sync)) {
mBinding.sync.qrcodeLayout.qrcode.setData(sync);
}
});
}
}

60
app/src/main/java/com/cobo/cold/ui/fragment/setup/SetupWatchWalletFragment.java

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.ui.fragment.setup;
import android.os.Bundle;
import android.view.View;
import com.cobo.cold.R;
import com.cobo.cold.databinding.SetupWatchWalletBinding;
import com.cobo.cold.viewmodel.SupportedWatchWallet;
import static com.cobo.cold.ui.fragment.Constants.KEY_TITLE;
import static com.cobo.cold.viewmodel.SupportedWatchWallet.getSupportedWatchWallet;
public class SetupWatchWalletFragment extends SetupVaultBaseFragment<SetupWatchWalletBinding> {
@Override
protected int setView() {
return R.layout.setup_watch_wallet;
}
@Override
protected void init(View view) {
super.init(view);
mBinding.complete.setOnClickListener(v -> complete());
}
private void complete() {
int navId = 0;
Bundle data = new Bundle();
SupportedWatchWallet selectWatchOnlyWallet = getSupportedWatchWallet(mActivity);
switch (selectWatchOnlyWallet) {
case ELECTRUM:
data.putInt(KEY_TITLE, R.string.select_address_format);
navId = R.id.action_to_selectAddressFormatFragment;
break;
case COBO:
case WASABI:
case BLUE:
navId = R.id.action_to_export_xpub_guide;
break;
}
navigate(navId, data);
}
}

2
app/src/main/java/com/cobo/cold/ui/views/DrawerAdapter.java

@ -43,8 +43,6 @@ public class DrawerAdapter extends RecyclerView.Adapter<DrawerAdapter.Holder> {
public OnItemClickListener listener;
private final List<DrawerItem> dataList = Arrays.asList(
new DrawerItem(R.id.drawer_wallet, R.drawable.drawer_wallet, R.string.drawer_menu_my_vault),
new DrawerItem(R.id.drawer_sync, R.drawable.drawer_asset_observation, R.string.drawer_menu_sync),
new DrawerItem(R.id.drawer_sdcard, R.drawable.drawer_sdcard, R.string.read_sdcard),
new DrawerItem(R.id.drawer_settings, R.drawable.drawer_setting, R.string.drawer_menu_setting),
new DrawerItem(R.id.drawer_about, R.drawable.drawer_about, R.string.drawer_menu_about)
);

163
app/src/main/java/com/cobo/cold/viewmodel/AddAddressViewModel.java

@ -19,57 +19,62 @@ package com.cobo.cold.viewmodel;
import android.app.Application;
import android.os.AsyncTask;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.databinding.ObservableField;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.cobo.coinlib.coins.AbsDeriver;
import com.cobo.coinlib.coins.BTC.Btc;
import com.cobo.coinlib.coins.BTC.Deriver;
import com.cobo.coinlib.exception.InvalidPathException;
import com.cobo.coinlib.path.Account;
import com.cobo.coinlib.path.AddressIndex;
import com.cobo.coinlib.path.CoinPath;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.callables.GetExtendedPublicKeyCallable;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.AddressEntity;
import com.cobo.cold.db.entity.CoinEntity;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class AddAddressViewModel extends AndroidViewModel {
private final DataRepository mRepo;
public CoinEntity coin;
private final ObservableField<Boolean> loading = new ObservableField<>();
private final MutableLiveData<Boolean> addComplete = new MutableLiveData<>();
private AddAddressViewModel(@NonNull Application application, DataRepository repository,
final long id) {
public AddAddressViewModel(@NonNull Application application) {
super(application);
mRepo = repository;
mRepo = ((MainApplication)application).getRepository();
}
public ObservableField<Boolean> getLoading() {
return loading;
}
public void addAddress(int count, Btc.AddressType type, int changeIndex) {
String hdPath;
switch (type) {
case P2SH:
hdPath = Coins.Account.P2SH.getPath();
break;
case SegWit:
hdPath = Coins.Account.SegWit.getPath();
break;
default:
hdPath = Coins.Account.P2PKH.getPath();
break;
}
List<AccountEntity> accounts = mRepo.loadAccountsForCoin(coin);
public void addAddress(List<String> addrs) {
loading.set(true);
new AddAddressTask(coin, mRepo, () -> {
loading.set(false);
addComplete.setValue(Boolean.TRUE);
}).execute(addrs.toArray(new String[0]));
}
accounts.stream()
.filter(account -> account.getHdPath().toUpperCase().equals(hdPath))
.findFirst()
.ifPresent(accountEntity -> new AddAddressTask(coin, mRepo,
() -> addComplete.setValue(Boolean.TRUE), accountEntity.getExPub(),
changeIndex).execute(count));
public void addAddress(CoinEntity coinEntity, DataRepository repo, String addrName) {
new AddAddressTask(coinEntity, repo, null).execute(addrName);
}
public CoinEntity getCoin(String coinId) {
@ -81,85 +86,79 @@ public class AddAddressViewModel extends AndroidViewModel {
return addComplete;
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
@NonNull
private final Application mApplication;
private final long mId;
private final DataRepository mRepository;
public Factory(@NonNull Application application, long id) {
mApplication = application;
mId = id;
mRepository = ((MainApplication) application).getRepository();
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new AddAddressViewModel(mApplication, mRepository, mId);
}
}
static class AddAddressTask extends AsyncTask<String, Void, Void> {
static class AddAddressTask extends AsyncTask<Integer, Void, Void> {
private final CoinEntity coinEntity;
private final DataRepository repo;
private final Runnable onComplete;
AddAddressTask(CoinEntity coinEntity, DataRepository repo, Runnable onComplete) {
private String xpub;
private int changeIndex;
AddAddressTask(CoinEntity coinEntity,
DataRepository repo,
Runnable onComplete,
@NonNull String xpub,
int changeIndex) {
this.coinEntity = coinEntity;
this.repo = repo;
this.onComplete = onComplete;
this.changeIndex = changeIndex;
this.xpub = xpub;
}
@Override
protected Void doInBackground(String... strings) {
AccountEntity defaultAccount = repo.loadAccountsForCoin(coinEntity).get(0);
int addressCount = coinEntity.getAddressCount();
Account account;
try {
account = Account.parseAccount(defaultAccount.getHdPath());
} catch (InvalidPathException e) {
return null;
}
String exPub = defaultAccount.getExPub();
if (TextUtils.isEmpty(exPub)) {
exPub = new GetExtendedPublicKeyCallable(account.toString()).call();
defaultAccount.setExPub(exPub);
protected Void doInBackground(Integer... count) {
AccountEntity accountEntity = repo.loadAccountsByXpub(coinEntity.getId(), xpub);
List<AddressEntity> address = repo.loadAddressSync(coinEntity.getCoinId());
Optional<AddressEntity> optional = address.stream()
.filter(addressEntity -> addressEntity.getPath()
.startsWith(accountEntity.getHdPath()+"/" + changeIndex))
.max((o1, o2) -> o1.getIndex() - o2.getIndex());
int index = -1;
if (optional.isPresent()) {
try {
AddressIndex addressIndex = CoinPath.parsePath(optional.get().getPath());
index = addressIndex.getValue();
} catch (InvalidPathException e) {
e.printStackTrace();
}
}
int addressCount = index + 1;
Btc.AddressType addressType = getAddressType(accountEntity);
List<AddressEntity> entities = new ArrayList<>();
for (int i = 0; i < strings.length; i++) {
for (int i = 0; i < count[0]; i++) {
AddressEntity addressEntity = new AddressEntity();
addressEntity.setPath(
account.external()
.address(i + addressCount).toString());
int coinType = account.getParent().getValue();
AbsDeriver deriver = AbsDeriver.newInstance(Coins.coinCodeOfIndex(coinType));
if (deriver != null) {
String addr = deriver.derive(exPub, 0, i + addressCount);
addressEntity.setAddressString(addr);
addressEntity.setCoinId(coinEntity.getCoinId());
addressEntity.setIndex(i + addressCount);
addressEntity.setName(strings[i]);
addressEntity.setBelongTo(coinEntity.getBelongTo());
entities.add(addressEntity);
}
addressEntity.setPath(accountEntity.getHdPath()+"/" + changeIndex+"/" + (addressCount + i));
String addr = new Deriver()
.derive(xpub, changeIndex, i + addressCount, addressType);
addressEntity.setAddressString(addr);
addressEntity.setCoinId(coinEntity.getCoinId());
addressEntity.setIndex(i + addressCount);
addressEntity.setName("BTC-" + (i + addressCount));
addressEntity.setBelongTo(coinEntity.getBelongTo());
entities.add(addressEntity);
}
coinEntity.setAddressCount(coinEntity.getAddressCount() + strings.length);
defaultAccount.setAddressLength(addressCount + strings.length);
repo.updateAccount(defaultAccount);
coinEntity.setAddressCount(coinEntity.getAddressCount() + count[0]);
accountEntity.setAddressLength(addressCount + count[0]);
repo.updateAccount(accountEntity);
repo.updateCoin(coinEntity);
repo.insertAddress(entities);
return null;
}
static Btc.AddressType getAddressType(AccountEntity accountEntity) {
String hdPath = accountEntity.getHdPath();
if (Coins.Account.P2SH.getPath().equals(hdPath)) {
return Btc.AddressType.P2SH;
} else if(Coins.Account.SegWit.getPath().equals(hdPath)) {
return Btc.AddressType.SegWit;
} else {
return Btc.AddressType.P2PKH;
}
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);

7
app/src/main/java/com/cobo/cold/viewmodel/CoinListViewModel.java

@ -25,7 +25,6 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
@ -36,23 +35,19 @@ import com.cobo.cold.model.Coin;
import com.cobo.cold.protocol.EncodeConfig;
import com.cobo.cold.protocol.builder.SyncBuilder;
import java.util.Comparator;
import java.util.List;
public class CoinListViewModel extends AndroidViewModel {
private final DataRepository mRepository;
private final MediatorLiveData<List<CoinEntity>> mObservableCoins;
public static final Comparator<CoinEntity> coinEntityComparator = (o1, o2) -> 0;
public CoinListViewModel(@NonNull Application application) {
super(application);
mObservableCoins = new MediatorLiveData<>();
mObservableCoins.setValue(null);
mRepository = ((MainApplication) application).getRepository();
mObservableCoins.addSource(mRepository.loadCoins(), mObservableCoins::setValue);
}
@ -78,7 +73,7 @@ public class CoinListViewModel extends AndroidViewModel {
return mRepository.loadTxs(coinId);
}
public List<AccountEntity> loadAccountForCoin(CoinEntity coin) {
private List<AccountEntity> loadAccountForCoin(CoinEntity coin) {
return mRepository.loadAccountsForCoin(coin);
}

50
app/src/main/java/com/cobo/cold/viewmodel/CoinViewModel.java

@ -26,6 +26,9 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.cobo.coinlib.exception.InvalidPathException;
import com.cobo.coinlib.path.AddressIndex;
import com.cobo.coinlib.path.CoinPath;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
@ -33,6 +36,7 @@ import com.cobo.cold.db.entity.AddressEntity;
import com.cobo.cold.db.entity.CoinEntity;
import java.util.List;
import java.util.stream.Collectors;
public class CoinViewModel extends AndroidViewModel {
@ -41,15 +45,42 @@ public class CoinViewModel extends AndroidViewModel {
private final LiveData<List<AddressEntity>> mObservableAddress;
public final ObservableField<CoinEntity> coin = new ObservableField<>();
private CoinViewModel(@NonNull Application application, DataRepository repository,
final long id, final String coinId) {
private CoinViewModel(@NonNull Application application,final String coinId) {
super(application);
mRepository = repository;
mObservableCoin = repository.loadCoin(id);
mObservableAddress = repository.loadAddress(coinId);
mRepository = ((MainApplication)application).getRepository();
mObservableCoin = mRepository.loadCoin(coinId);
mObservableAddress = mRepository.loadAddress(coinId);
}
public List<AddressEntity> filterChangeAddress(List<AddressEntity> addressEntities) {
return addressEntities.stream()
.filter(this::isChangeAddress)
.collect(Collectors.toList());
}
public List<AddressEntity> filterReceiveAddress(List<AddressEntity> addressEntities) {
return addressEntities.stream()
.filter(addressEntity -> !isChangeAddress(addressEntity))
.collect(Collectors.toList());
}
public List<AddressEntity> filterByAccountHdPath(List<AddressEntity> addressEntities, String hdPath) {
return addressEntities.stream()
.filter(addressEntity -> addressEntity.getPath().toUpperCase().startsWith(hdPath))
.collect(Collectors.toList());
}
private boolean isChangeAddress(AddressEntity addressEntity) {
String path = addressEntity.getPath();
try {
AddressIndex addressIndex = CoinPath.parsePath(path);
return !addressIndex.getParent().isExternal();
} catch (InvalidPathException e) {
e.printStackTrace();
}
return false;
}
public LiveData<CoinEntity> getObservableCoin() {
return mObservableCoin;
}
@ -69,22 +100,19 @@ public class CoinViewModel extends AndroidViewModel {
public static class Factory extends ViewModelProvider.NewInstanceFactory {
@NonNull
private final Application mApplication;
private final long mId;
private final String mCoinId;
private final DataRepository mRepository;
public Factory(@NonNull Application application, long id, String coinId) {
public Factory(@NonNull Application application, String coinId) {
mApplication = application;
mId = id;
mCoinId = coinId;
mRepository = ((MainApplication) application).getRepository();
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new CoinViewModel(mApplication, mRepository, mId, mCoinId);
return (T) new CoinViewModel(mApplication, mCoinId);
}
}
}

122
app/src/main/java/com/cobo/cold/viewmodel/ElectrumViewModel.java

@ -18,37 +18,18 @@
package com.cobo.cold.viewmodel;
import android.app.Application;
import android.content.Context;
import android.os.Handler;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cobo.coinlib.Util;
import com.cobo.coinlib.coins.AbsDeriver;
import com.cobo.coinlib.coins.BTC.Btc;
import com.cobo.coinlib.coins.BTC.Electrum.ElectrumTx;
import com.cobo.coinlib.coins.BTC.Electrum.TransactionInput;
import com.cobo.coinlib.coins.BTC.Electrum.TransactionOutput;
import com.cobo.coinlib.exception.InvalidPathException;
import com.cobo.coinlib.path.Account;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.R;
import com.cobo.cold.databinding.CommonModalBinding;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.ui.modal.ExportToSdcardDialog;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.FileUtils;
import com.cobo.cold.update.utils.Storage;
@ -66,73 +47,16 @@ import java.util.regex.Pattern;
public class ElectrumViewModel extends AndroidViewModel {
public static final String ELECTRUM_SIGN_ID = "electrum_sign_id";
private static final int DEFAULT_CHANGE_ADDRESS_NUM = 100;
private static Pattern signedTxnPattern = Pattern.compile("^signed_[0-9a-fA-F]{8}.txn$");
private final DataRepository mRepo;
private MutableLiveData<String> exPub = new MutableLiveData<>();
private Storage storage;
private MutableLiveData<List<String>> changeAddress = new MutableLiveData<>();
private String xpub;
public ElectrumViewModel(@NonNull Application application) {
super(application);
mRepo = MainApplication.getApplication().getRepository();
storage = Storage.createByEnvironment(application);
deriveChangeAddress();
}
private void deriveChangeAddress() {
AppExecutors.getInstance().networkIO().execute(()->{
if (TextUtils.isEmpty(xpub)) {
xpub = new ExpubInfo().invoke().expub;
}
List<String> changes = new ArrayList<>();
AbsDeriver btcDeriver = new Btc.Deriver();
for (int i = 0; i < DEFAULT_CHANGE_ADDRESS_NUM; i++) {
changes.add(btcDeriver.derive(xpub,1, i));
}
changeAddress.postValue(changes);
});
}
public LiveData<List<String>> getChangeAddress() {
return changeAddress;
}
public static boolean hasSdcard(Context context) {
Storage storage = Storage.createByEnvironment(context);
return storage != null && storage.getExternalDir() != null;
}
public static boolean writeToSdcard(Storage storage, String content, String fileName) {
File file = new File(storage.getElectrumDir(), fileName);
return FileUtils.writeString(file, content);
}
public static void showNoSdcardModal(AppCompatActivity activity) {
ModalDialog modalDialog = ModalDialog.newInstance();
CommonModalBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(activity), R.layout.common_modal,
null, false);
binding.title.setText(R.string.hint);
binding.subTitle.setText(R.string.insert_sdcard_hint);
binding.close.setVisibility(View.GONE);
binding.confirm.setText(R.string.know);
binding.confirm.setOnClickListener(vv -> modalDialog.dismiss());
modalDialog.setBinding(binding);
modalDialog.show(activity.getSupportFragmentManager(), "");
}
public static void exportSuccess(AppCompatActivity activity, Runnable runnable) {
ExportToSdcardDialog dialog = new ExportToSdcardDialog();
dialog.show(activity.getSupportFragmentManager(), "");
new Handler().postDelayed(() -> {
dialog.dismiss();
if (runnable != null) {
runnable.run();
}
}, 1000);
}
public static JSONObject adapt(ElectrumTx tx) throws JSONException {
@ -174,31 +98,6 @@ public class ElectrumViewModel extends AndroidViewModel {
}
}
public String getXpub() {
return xpub;
}
public LiveData<String> getMasterPublicKey() {
AppExecutors.getInstance().diskIO().execute(() -> {
ExpubInfo expubInfo = new ExpubInfo().invoke();
String hdPath = expubInfo.getHdPath();
String expub = expubInfo.getExpub();
xpub = expub;
try {
Account account = Account.parseAccount(hdPath);
if (account.getParent().getParent().getValue() == 49 && expub.startsWith("xpub")) {
exPub.postValue(Util.convertXpubToYpub(expub));
} else if (expub.startsWith("ypub")) {
exPub.postValue(expub);
}
} catch (InvalidPathException e) {
e.printStackTrace();
}
});
return exPub;
}
private boolean isSignedTxn(String fileName) {
Matcher matcher = signedTxnPattern.matcher(fileName);
return matcher.matches();
@ -239,25 +138,4 @@ public class ElectrumViewModel extends AndroidViewModel {
});
return txnHex;
}
private class ExpubInfo {
private String hdPath;
private String expub;
public String getHdPath() {
return hdPath;
}
public String getExpub() {
return expub;
}
public ExpubInfo invoke() {
CoinEntity btc = mRepo.loadCoinSync(Coins.coinIdFromCoinCode("BTC"));
AccountEntity accountEntity = mRepo.loadAccountsForCoin(btc).get(0);
hdPath = accountEntity.getHdPath();
expub = accountEntity.getExPub();
return this;
}
}
}

271
app/src/main/java/com/cobo/cold/viewmodel/GlobalViewModel.java

@ -0,0 +1,271 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.viewmodel;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cobo.coinlib.Util;
import com.cobo.coinlib.coins.BTC.Btc;
import com.cobo.coinlib.coins.BTC.Deriver;
import com.cobo.coinlib.exception.InvalidPathException;
import com.cobo.coinlib.path.Account;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.BuildConfig;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.R;
import com.cobo.cold.Utilities;
import com.cobo.cold.callables.GetExtendedPublicKeyCallable;
import com.cobo.cold.callables.GetMasterFingerprintCallable;
import com.cobo.cold.databinding.CommonModalBinding;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.ui.modal.ExportToSdcardDialog;
import com.cobo.cold.ui.modal.ModalDialog;
import com.cobo.cold.update.utils.FileUtils;
import com.cobo.cold.update.utils.Storage;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static com.cobo.cold.ui.fragment.setting.MainPreferenceFragment.SETTING_ADDRESS_FORMAT;
public class GlobalViewModel extends AndroidViewModel {
private static final int DEFAULT_CHANGE_ADDRESS_NUM = 100;
private final DataRepository mRepo;
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sp, key) -> {
if (SETTING_ADDRESS_FORMAT.equals(key)) {
deriveChangeAddress();
}
};
private MutableLiveData<String> exPub = new MutableLiveData<>();
private MutableLiveData<List<String>> changeAddress = new MutableLiveData<>();
private String xpub;
private AccountEntity accountEntity;
public GlobalViewModel(@NonNull Application application) {
super(application);
mRepo = MainApplication.getApplication().getRepository();
deriveChangeAddress();
Utilities.getPrefs(application).registerOnSharedPreferenceChangeListener(listener);
}
public static Coins.Account getAccount(Context context) {
SharedPreferences pref = Utilities.getPrefs(context);
String type = pref.getString(SETTING_ADDRESS_FORMAT, Coins.Account.P2SH.getType());
for (Coins.Account account: Coins.Account.values()) {
if (type.equals(account.getType())) {
return account;
}
}
return Coins.Account.P2SH;
}
public static Btc.AddressType getAddressType(Context context) {
switch (getAccount(context)) {
case P2SH:
return Btc.AddressType.P2SH;
case P2PKH:
return Btc.AddressType.P2PKH;
case SegWit:
return Btc.AddressType.SegWit;
}
return Btc.AddressType.SegWit;
}
public static String getAddressFormat(Context context) {
switch (getAccount(context)) {
case SegWit:
return context.getString(R.string.native_segwit);
case P2PKH:
return context.getString(R.string.p2pkh);
case P2SH:
return context.getString(R.string.nested_segwit);
}
return context.getString(R.string.nested_segwit);
}
public static JSONObject getXpubInfo(Context activity) {
JSONObject xpubInfo = new JSONObject();
Coins.Account account = getAccount(activity);
String xpub = new GetExtendedPublicKeyCallable(account.getPath()).call();
String masterKeyFingerprint = new GetMasterFingerprintCallable().call();
try {
xpubInfo.put("ExtPubKey", xpub);
xpubInfo.put("MasterFingerprint", masterKeyFingerprint);
xpubInfo.put("CoboVaultVersion", BuildConfig.VERSION_NAME);
} catch (JSONException e) {
e.printStackTrace();
}
return xpubInfo;
}
private void deriveChangeAddress() {
AppExecutors.getInstance().networkIO().execute(()->{
ExpubInfo expubInfo = new ExpubInfo().getExPubInfo();
xpub = expubInfo.expub;
String path = expubInfo.hdPath;
List<String> changes = new ArrayList<>();
Btc.AddressType type;
if (Coins.Account.P2SH.getPath().equals(path)) {
type = Btc.AddressType.P2SH;
} else if (Coins.Account.SegWit.getPath().equals(path)) {
type = Btc.AddressType.SegWit;
} else {
type = Btc.AddressType.P2PKH;
}
Deriver btcDeriver = new Deriver();
for (int i = 0; i < DEFAULT_CHANGE_ADDRESS_NUM; i++) {
changes.add(btcDeriver.derive(xpub,1, i, type));
}
changeAddress.postValue(changes);
});
}
public LiveData<String> getExtendPublicKey() {
AppExecutors.getInstance().diskIO().execute(() -> {
ExpubInfo expubInfo = new ExpubInfo().getExPubInfo();
String hdPath = expubInfo.getHdPath();
String extPub = expubInfo.getExpub();
try {
Account account = Account.parseAccount(hdPath);
if (account.getParent().getParent().getValue() == 49
&& extPub.startsWith("xpub")) {
exPub.postValue(Util.convertXpubToYpub(extPub));
} else if (extPub.startsWith("ypub")) {
exPub.postValue(extPub);
} else if (account.getParent().getParent().getValue() == 84
&& extPub.startsWith("xpub")) {
exPub.postValue(Util.convertXpubToZpub(extPub));
}
} catch (InvalidPathException e) {
e.printStackTrace();
}
});
return exPub;
}
public AccountEntity getAccountEntity() {
return accountEntity;
}
public LiveData<List<String>> getChangeAddress() {
deriveChangeAddress();
return changeAddress;
}
public String getXpub() {
return xpub;
}
public static boolean hasSdcard(Context context) {
Storage storage = Storage.createByEnvironment(context);
return storage != null && storage.getExternalDir() != null;
}
public static boolean writeToSdcard(Storage storage, String content, String fileName) {
File file = new File(storage.getElectrumDir(), fileName);
return FileUtils.writeString(file, content);
}
public static void showNoSdcardModal(AppCompatActivity activity) {
ModalDialog modalDialog = ModalDialog.newInstance();
CommonModalBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(activity), R.layout.common_modal,
null, false);
binding.title.setText(R.string.hint);
binding.subTitle.setText(R.string.insert_sdcard_hint);
binding.close.setVisibility(View.GONE);
binding.confirm.setText(R.string.know);
binding.confirm.setOnClickListener(vv -> modalDialog.dismiss());
modalDialog.setBinding(binding);
modalDialog.show(activity.getSupportFragmentManager(), "");
}
public static void exportSuccess(AppCompatActivity activity, Runnable runnable) {
ExportToSdcardDialog dialog = new ExportToSdcardDialog();
dialog.show(activity.getSupportFragmentManager(), "");
new Handler().postDelayed(() -> {
dialog.dismiss();
if (runnable != null) {
runnable.run();
}
}, 1000);
}
private class ExpubInfo {
private String hdPath;
private String expub;
public String getHdPath() {
return hdPath;
}
public String getExpub() {
return expub;
}
public ExpubInfo getExPubInfo() {
CoinEntity btc = mRepo.loadCoinSync(Coins.BTC.coinId());
SharedPreferences sp = Utilities.getPrefs(getApplication());
List<AccountEntity> accounts = mRepo.loadAccountsForCoin(btc);
String format = sp.getString(SETTING_ADDRESS_FORMAT, Coins.Account.P2SH.getType());
Coins.Account account;
if (Coins.Account.P2SH.getType().equals(format)) {
account = Coins.Account.P2SH;
} else if(Coins.Account.SegWit.getType().equals(format)) {
account = Coins.Account.SegWit;
} else {
account = Coins.Account.P2PKH;
}
for (AccountEntity entity : accounts) {
if (entity.getHdPath().equals(account.getPath())) {
accountEntity = entity;
hdPath = entity.getHdPath();
expub = entity.getExPub();
}
}
return this;
}
}
}

127
app/src/main/java/com/cobo/cold/viewmodel/PsbtViewModel.java

@ -0,0 +1,127 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.viewmodel;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.callables.GetMasterFingerprintCallable;
import com.cobo.cold.update.utils.Storage;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PsbtViewModel extends AndroidViewModel {
public static final String WASABI_SIGN_ID = "wasabi_sign_id";
private static Pattern signedTxnPattern = Pattern.compile("^signed_[0-9a-fA-F]{8}.psbt$");
private Storage storage;
public PsbtViewModel(@NonNull Application application) {
super(application);
storage = Storage.createByEnvironment(application);
}
public static JSONObject adapt(JSONObject psbt) throws JSONException, WatchWalletNotMatchException {
JSONObject object = new JSONObject();
JSONArray inputs = new JSONArray();
JSONArray outputs = new JSONArray();
adaptInputs(psbt.getJSONArray("inputs"), inputs);
if (inputs.length() < 1) {
throw new WatchWalletNotMatchException("no input match masterFingerprint");
}
adaptOutputs(psbt.getJSONArray("outputs"), outputs);
object.put("inputs", inputs);
object.put("outputs", outputs);
return object;
}
private static void adaptInputs(JSONArray psbtInputs, JSONArray inputs) throws JSONException {
String masterKeyFingerprint = new GetMasterFingerprintCallable().call();
for (int i = 0; i < psbtInputs.length(); i++) {
JSONObject psbtInput = psbtInputs.getJSONObject(i);
JSONObject in = new JSONObject();
JSONObject utxo = new JSONObject();
in.put("hash", psbtInput.getString("txId"));
in.put("index", psbtInput.getInt("index"));
JSONArray bip32Derivation = psbtInput.getJSONArray("hdPath");
for (int j = 0; j < bip32Derivation.length(); j++) {
JSONObject item = bip32Derivation.getJSONObject(j);
if (item.getString("masterFingerprint").equals(masterKeyFingerprint)) {
utxo.put("publicKey", item.getString("pubkey"));
utxo.put("value", psbtInput.optInt("value"));
in.put("utxo", utxo);
in.put("ownerKeyPath", item.getString("path"));
in.put("masterFingerprint", item.getString("masterFingerprint"));
inputs.put(in);
break;
}
}
}
}
private static void adaptOutputs(JSONArray psbtOutputs, JSONArray outputs) throws JSONException {
for(int i = 0; i < psbtOutputs.length(); i++) {
JSONObject psbtOutput = psbtOutputs.getJSONObject(i);
JSONObject out = new JSONObject();
out.put("address", psbtOutput.getString("address"));
out.put("value", psbtOutput.getInt("value"));
outputs.put(out);
}
}
private boolean isSignedPsbt(String fileName) {
Matcher matcher = signedTxnPattern.matcher(fileName);
return matcher.matches();
}
public LiveData<List<String>> loadUnsignPsbt() {
MutableLiveData<List<String>> result = new MutableLiveData<>();
AppExecutors.getInstance().diskIO().execute(() -> {
List<String> fileList = new ArrayList<>();
if (storage != null) {
File[] files = storage.getElectrumDir().listFiles();
if (files != null) {
for (File f : files) {
if (f.getName().endsWith(".psbt")
&& !isSignedPsbt(f.getName())) {
fileList.add(f.getName());
}
}
}
}
result.postValue(fileList);
});
return result;
}
}

3
app/src/main/java/com/cobo/cold/viewmodel/PublicKeyViewModel.java

@ -24,12 +24,9 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.AddressEntity;
import com.cobo.cold.db.entity.CoinEntity;
import java.util.List;

84
app/src/main/java/com/cobo/cold/viewmodel/QrScanViewModel.java

@ -29,14 +29,11 @@ import androidx.lifecycle.ViewModelProvider;
import com.cobo.coinlib.exception.CoinNotFindException;
import com.cobo.coinlib.exception.InvalidTransactionException;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.BuildConfig;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.R;
import com.cobo.cold.callables.GetUuidCallable;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.encryptioncore.utils.ByteFormatter;
import com.cobo.cold.protocol.ZipUtil;
import com.cobo.cold.protocol.parser.ProtoParser;
@ -44,14 +41,11 @@ import com.cobo.cold.scan.ScannedData;
import com.cobo.cold.ui.fragment.main.QRCodeScanFragment;
import com.cobo.cold.update.utils.Digest;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.spongycastle.util.encoders.Base64;
import org.spongycastle.util.encoders.DecoderException;
import java.util.List;
import static com.cobo.cold.Utilities.IS_SETUP_VAULT;
import static com.cobo.cold.ui.fragment.main.TxConfirmFragment.KEY_TX_DATA;
import static com.cobo.cold.ui.fragment.setup.WebAuthResultFragment.WEB_AUTH_DATA;
@ -73,7 +67,7 @@ public class QrScanViewModel extends AndroidViewModel {
public void handleDecode(QRCodeScanFragment owner, ScannedData[] res)
throws InvalidTransactionException, JSONException, CoinNotFindException,
UuidNotMatchException, UnknowQrCodeException {
UuidNotMatchException, UnknowQrCodeException, WatchWalletNotMatchException {
this.fragment = owner;
String valueType = res[0].valueType;
JSONObject object = parseToJson(res, valueType);
@ -88,7 +82,7 @@ public class QrScanViewModel extends AndroidViewModel {
CoinNotFindException,
JSONException,
UuidNotMatchException,
UnknowQrCodeException {
UnknowQrCodeException, WatchWalletNotMatchException {
logObject(object);
String type = object.getString("type");
@ -99,9 +93,6 @@ public class QrScanViewModel extends AndroidViewModel {
case "TYPE_SIGN_TX":
handleSign(object);
break;
case "TYPE_SYNC":
handleSync(object);
break;
default:
throw new UnknowQrCodeException("unknow qrcode type " + type);
}
@ -119,69 +110,6 @@ public class QrScanViewModel extends AndroidViewModel {
}
}
private void handleSync(JSONObject object) throws JSONException, UuidNotMatchException {
checkUuid(object);
JSONObject sync = object.getJSONObject("sync");
JSONArray coins = sync.getJSONArray("coins");
AppExecutors.getInstance().diskIO().execute(() -> {
List<CoinEntity> coinEntities = repository.loadCoinsSync();
for (int i = 0; i < coins.length(); i++) {
try {
JSONObject coinObj = coins.getJSONObject(i);
CoinEntity coinEntity = matchCoinEntity(coinEntities, coinObj.getString("id"));
if (coinEntity == null) {
continue;
}
sync(coinObj, coinEntity);
} catch (JSONException e) {
e.printStackTrace();
}
}
AppExecutors.getInstance().mainThread().execute(() -> fragment.navigateUp());
});
}
private void sync(JSONObject coinObj, CoinEntity coinEntity) throws JSONException {
List<AccountEntity> accountEntities = repository.loadAccountsForCoin(coinEntity);
JSONArray accountObjs = coinObj.getJSONArray("accounts");
for (int i = 0; i < accountObjs.length(); i++) {
JSONObject accountObj = accountObjs.getJSONObject(i);
AccountEntity accountEntity = matchAccountEntity(accountEntities, accountObj.getString("hdPath"));
if (accountEntity == null) {
continue;
}
int addressLengthNew = accountObj.getInt("addressLength");
int addressLengthCurrent = accountEntity.getAddressLength();
if (addressLengthNew > addressLengthCurrent) {
String[] names = new String[addressLengthNew - addressLengthCurrent];
int index = 0;
for (int j = addressLengthCurrent; j < addressLengthNew; j++) {
names[index++] = coinEntity.getCoinCode() + "-" + (j + 1);
}
new AddAddressViewModel.AddAddressTask(coinEntity, repository, null).execute(names);
}
}
}
private AccountEntity matchAccountEntity(List<AccountEntity> accounts, String hdPath) {
for (AccountEntity a : accounts) {
if (a.getHdPath().equals(hdPath)) {
return a;
}
}
return null;
}
private CoinEntity matchCoinEntity(List<CoinEntity> coins, String code) {
for (CoinEntity c : coins) {
if (code.equals(c.getCoinCode()) || code.equals(c.getCoinId())) {
return c;
}
}
return null;
}
private void handleWebAuth(JSONObject object) throws JSONException {
String data = object.getString("data");
Bundle bundle = new Bundle();
@ -197,7 +125,13 @@ public class QrScanViewModel extends AndroidViewModel {
private void handleSign(JSONObject object)
throws InvalidTransactionException,
CoinNotFindException,
UuidNotMatchException {
UuidNotMatchException, WatchWalletNotMatchException {
if (SupportedWatchWallet.getSupportedWatchWallet(getApplication())
!= SupportedWatchWallet.COBO) {
throw new WatchWalletNotMatchException("");
}
checkUuid(object);
try {
String coinCode = object.getJSONObject("signTx")

35
app/src/main/java/com/cobo/cold/viewmodel/SetupVaultViewModel.java

@ -28,7 +28,6 @@ import androidx.lifecycle.MutableLiveData;
import com.cobo.coinlib.MnemonicUtils;
import com.cobo.coinlib.utils.Bip39;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
@ -204,26 +203,32 @@ public class SetupVaultViewModel extends AndroidViewModel {
AppExecutors.getInstance().diskIO().execute(() -> {
for (CoinEntity coin : coins) {
CoinEntity coinEntity = mRepository.loadCoinSync(coin.getCoinId());
if (coinEntity != null) {
continue;
long id;
if (coinEntity == null) {
id = mRepository.insertCoin(coin);
coinEntity = mRepository.loadCoinSync(coin.getCoinId());
} else {
id = coinEntity.getId();
coinEntity = mRepository.loadCoinSync(coin.getCoinId());
}
String xPub = new GetExtendedPublicKeyCallable(coin.getAccounts().get(0).getHdPath()).call();
coin.setExPub(xPub);
long id = mRepository.insertCoin(coin);
coin.setId(id);
boolean isFirstAccount = true;
List<AccountEntity> existsAccount = mRepository.loadAccountsForCoin(coinEntity);
for (AccountEntity account : coin.getAccounts()) {
if (!isFirstAccount) {
xPub = new GetExtendedPublicKeyCallable(account.getHdPath()).call();
if (existsAccount.stream()
.anyMatch(exist -> exist.getHdPath().equals(account.getHdPath()))) {
continue;
}
isFirstAccount = false;
String xPub = new GetExtendedPublicKeyCallable(account.getHdPath()).call();
account.setCoinId(id);
account.setExPub(xPub);
mRepository.insertAccount(account);
if (!Coins.showPublicKey(coin.getCoinCode())) {
new AddAddressViewModel.AddAddressTask(coin, mRepository, null)
.execute(coin.getCoinCode() + "-1");
}
new AddAddressViewModel.AddAddressTask(coinEntity, mRepository,
null, xPub,0)
.execute(1);
new AddAddressViewModel.AddAddressTask(coinEntity, mRepository,
null, xPub,1)
.execute(1);
}
}
if (onComplete != null) {

60
app/src/main/java/com/cobo/cold/viewmodel/SupportedWatchWallet.java

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.viewmodel;
import android.content.Context;
import com.cobo.cold.R;
import com.cobo.cold.Utilities;
import static com.cobo.cold.ui.fragment.setting.MainPreferenceFragment.SETTING_CHOOSE_WATCH_WALLET;
public enum SupportedWatchWallet {
COBO("0"),
ELECTRUM("1"),
WASABI("2"),
BLUE("3"),
GENERIC("4");
private String walletId;
SupportedWatchWallet(String walletId) {
this.walletId = walletId;
}
public String getWalletId() {
return walletId;
}
public String getWalletName(Context context) {
return context.getResources()
.getStringArray(R.array.watch_wallet_list)[Integer.parseInt(walletId)];
}
public static SupportedWatchWallet getSupportedWatchWallet(Context context) {
String wallet = Utilities.getPrefs(context)
.getString(SETTING_CHOOSE_WATCH_WALLET, ELECTRUM.getWalletId());
SupportedWatchWallet selectWatchWallet = ELECTRUM;
for (SupportedWatchWallet watchWallet: SupportedWatchWallet.values()) {
if (watchWallet.getWalletId().equals(wallet)) {
selectWatchWallet = watchWallet;
break;
}
}
return selectWatchWallet;
}
}

247
app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java

@ -28,15 +28,16 @@ import androidx.lifecycle.MutableLiveData;
import com.cobo.coinlib.Util;
import com.cobo.coinlib.coins.AbsCoin;
import com.cobo.coinlib.coins.AbsDeriver;
import com.cobo.coinlib.coins.AbsTx;
import com.cobo.coinlib.coins.BTC.Btc;
import com.cobo.coinlib.coins.BTC.BtcImpl;
import com.cobo.coinlib.coins.BTC.Deriver;
import com.cobo.coinlib.coins.BTC.Electrum.ElectrumTx;
import com.cobo.coinlib.coins.BTC.UtxoTx;
import com.cobo.coinlib.exception.InvalidPathException;
import com.cobo.coinlib.exception.InvalidTransactionException;
import com.cobo.coinlib.interfaces.SignCallback;
import com.cobo.coinlib.interfaces.SignPsbtCallback;
import com.cobo.coinlib.interfaces.Signer;
import com.cobo.coinlib.path.AddressIndex;
import com.cobo.coinlib.path.CoinPath;
@ -44,11 +45,11 @@ import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.Utilities;
import com.cobo.cold.callables.ClearTokenCallable;
import com.cobo.cold.callables.GetMessageCallable;
import com.cobo.cold.callables.GetPasswordTokenCallable;
import com.cobo.cold.callables.VerifyFingerprintCallable;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.AddressEntity;
import com.cobo.cold.db.entity.CoinEntity;
import com.cobo.cold.db.entity.TxEntity;
@ -69,6 +70,7 @@ import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@ -76,8 +78,11 @@ import java.util.concurrent.Executors;
import java.util.stream.Stream;
import static com.cobo.coinlib.coins.BTC.Electrum.TxUtils.isMasterPublicKeyMatch;
import static com.cobo.cold.viewmodel.AddAddressViewModel.AddAddressTask.getAddressType;
import static com.cobo.cold.viewmodel.ElectrumViewModel.ELECTRUM_SIGN_ID;
import static com.cobo.cold.viewmodel.ElectrumViewModel.adapt;
import static com.cobo.cold.viewmodel.GlobalViewModel.getAccount;
import static com.cobo.cold.viewmodel.PsbtViewModel.WASABI_SIGN_ID;
public class TxConfirmViewModel extends AndroidViewModel {
@ -156,14 +161,21 @@ public class TxConfirmViewModel extends AndroidViewModel {
public void parseTxnData(String txnData) {
AppExecutors.getInstance().networkIO().execute(() -> {
try {
String xpub = mRepository.loadCoinEntityByCoinCode(Coins.BTC.coinCode()).getExPub();
ElectrumTx tx = ElectrumTx.parse(Hex.decode(txnData));
if (!isMasterPublicKeyMatch(xpub, tx)) {
throw new XpubNotMatchException("xpub not match");
}
CoinEntity coinEntity = mRepository.loadCoinSync(Coins.BTC.coinId());
AccountEntity accountEntity =
mRepository.loadAccountsByPath(coinEntity.getId(), getAccount(getApplication()).getPath());
JSONObject signTx = parseElectrumTxHex(tx);
parseTxData(signTx.toString());
if (accountEntity != null) {
String xpub = accountEntity.getExPub();
ElectrumTx tx = ElectrumTx.parse(Hex.decode(txnData));
if (!isMasterPublicKeyMatch(xpub, tx)) {
throw new XpubNotMatchException("xpub not match");
}
JSONObject signTx = parseElectrumTxHex(tx);
parseTxData(signTx.toString());
}
} catch (ElectrumTx.SerializationException | JSONException | DecoderException e) {
e.printStackTrace();
parseTxException.postValue(new InvalidTransactionException("invalid transaction"));
@ -179,7 +191,7 @@ public class TxConfirmViewModel extends AndroidViewModel {
TransactionProtoc.SignTransaction.Builder builder = TransactionProtoc.SignTransaction.newBuilder();
builder.setCoinCode(Coins.BTC.coinCode())
.setSignId(ELECTRUM_SIGN_ID)
.setTimestamp(generateElectrumTimestamp())
.setTimestamp(generateAutoIncreaseId())
.setDecimal(8);
String signTransaction = new JsonFormat().printToString(builder.build());
JSONObject signTx = new JSONObject(signTransaction);
@ -187,7 +199,48 @@ public class TxConfirmViewModel extends AndroidViewModel {
return signTx;
}
private long generateElectrumTimestamp() {
public void parsePsbtBase64(String psbtBase64) {
AppExecutors.getInstance().networkIO().execute(() -> {
Btc btc = new Btc(new BtcImpl());
JSONObject psbtTx = btc.parsePsbt(psbtBase64);
if (psbtTx == null) {
parseTxException.postValue(new InvalidTransactionException("parse failed,invalid psbt data"));
return;
}
try {
JSONObject adaptTx = PsbtViewModel.adapt(psbtTx);
if (adaptTx.getJSONArray("inputs").length() == 0) {
parseTxException.postValue(
new InvalidTransactionException("master fingerprint not match, or nothing can be sign"));
}
JSONObject signTx = parseWasabiTx(adaptTx);
parseTxData(signTx.toString());
} catch (JSONException e) {
e.printStackTrace();
parseTxException.postValue(new InvalidTransactionException("adapt failed,invalid psbt data"));
} catch (WatchWalletNotMatchException e) {
e.printStackTrace();
parseTxException.postValue(e);
}
});
}
private JSONObject parseWasabiTx(JSONObject adaptTx) throws JSONException {
TransactionProtoc.SignTransaction.Builder builder = TransactionProtoc.SignTransaction.newBuilder();
builder.setCoinCode(Coins.BTC.coinCode())
.setSignId(WASABI_SIGN_ID)
.setTimestamp(generateAutoIncreaseId())
.setDecimal(8);
String signTransaction = new JsonFormat().printToString(builder.build());
JSONObject signTx = new JSONObject(signTransaction);
signTx.put("btcTx", adaptTx);
return signTx;
}
private long generateAutoIncreaseId() {
List<TxEntity> txEntityList = mRepository.loadElectrumTxsSync(Coins.BTC.coinId());
if (txEntityList == null || txEntityList.isEmpty()) {
return 0;
@ -205,14 +258,23 @@ public class TxConfirmViewModel extends AndroidViewModel {
}
String hdPath = changeAddressInfo.hdPath;
String address = changeAddressInfo.address;
String exPub = mRepository.loadCoinEntityByCoinCode(utxoTx.getCoinCode()).getExPub();
AbsDeriver deriver = AbsDeriver.newInstance(utxoTx.getCoinCode());
String accountHdPath = getAccountHdPath(changeAddressInfo.hdPath);
AccountEntity accountEntity = getAccountEntityByPath(accountHdPath,
mRepository.loadCoinEntityByCoinCode(utxoTx.getCoinCode()));
if (accountEntity == null) {
return false;
}
String exPub = accountEntity.getExPub();
Deriver deriver = new Deriver();
try {
AddressIndex addressIndex = CoinPath.parsePath(hdPath);
int change = addressIndex.getParent().getValue();
int index = addressIndex.getValue();
String expectAddress = Objects.requireNonNull(deriver).derive(exPub, change, index);
String expectAddress = Objects.requireNonNull(deriver).derive(exPub, change,
index, getAddressType(accountEntity));
return address.equals(expectAddress);
} catch (InvalidPathException e) {
e.printStackTrace();
@ -248,8 +310,9 @@ public class TxConfirmViewModel extends AndroidViewModel {
JSONArray inputsClone = new JSONArray();
JSONArray inputs = ((UtxoTx) transaction).getInputs();
CoinEntity coin = mRepository.loadCoinSync(Coins.coinIdFromCoinCode(coinCode));
String expub = mRepository.loadAccountsForCoin(coin).get(0).getExPub();
CoinEntity coinEntity = mRepository.loadCoinSync(Coins.coinIdFromCoinCode(transaction.getCoinCode()));
AccountEntity accountEntity =
mRepository.loadAccountsByPath(coinEntity.getId(), getAccount(getApplication()).getPath());
for (int i = 0; i < inputs.length(); i++) {
JSONObject input = inputs.getJSONObject(i);
@ -259,20 +322,18 @@ public class TxConfirmViewModel extends AndroidViewModel {
int index = addressIndex.getValue();
int change = addressIndex.getParent().getValue();
String from = AbsDeriver.newInstance(transaction.getCoinCode()).derive(expub,change,index);
String from = new Deriver().derive(accountEntity.getExPub()
,change,index, getAddressType(accountEntity));
inputsClone.put(new JSONObject().put("value", value)
.put("address",from));
}
return inputsClone.toString();
}
} catch (JSONException e) {
e.printStackTrace();
} catch (InvalidPathException e) {
} catch (JSONException | InvalidPathException e) {
e.printStackTrace();
}
return Stream.of(externalPath)
.distinct()
.map(path -> mRepository.loadAddressBypath(path).getAddressString())
@ -296,15 +357,6 @@ public class TxConfirmViewModel extends AndroidViewModel {
return false;
}
private boolean isInternalPath(@NonNull String path) {
try {
return !CoinPath.parsePath(path).getParent().isExternal();
} catch (InvalidPathException e) {
e.printStackTrace();
}
return false;
}
private void ensureAddressExist(String[] paths) {
if (paths == null || paths.length == 0) {
return;
@ -321,7 +373,7 @@ public class TxConfirmViewModel extends AndroidViewModel {
}
AddressEntity address = mRepository.loadAddressBypath(maxIndexHdPath);
if (address == null) {
addAddress(getAddressIndex(maxIndexHdPath));
addAddress(maxIndexHdPath);
}
}
@ -329,20 +381,44 @@ public class TxConfirmViewModel extends AndroidViewModel {
return addingAddress;
}
private void addAddress(int addressIndex) {
CoinEntity coin = mRepository.loadCoinSync(Coins.coinIdFromCoinCode(coinCode));
int addressLength = mRepository.loadAccountsForCoin(coin).get(0).getAddressLength();
private void addAddress(String hdPath) {
String accountHdPath;
int pathIndex;
try {
AddressIndex index = CoinPath.parsePath(hdPath);
pathIndex = index.getValue();
accountHdPath = index.getParent().getParent().toString();
} catch (InvalidPathException e) {
e.printStackTrace();
return;
}
if (addressLength < addressIndex + 1) {
String[] names = new String[addressIndex + 1 - addressLength];
int index = 0;
for (int i = addressLength; i < addressIndex + 1; i++) {
names[index++] = coinCode + "-" + (i + 1);
CoinEntity coin = mRepository.loadCoinSync(Coins.coinIdFromCoinCode(coinCode));
AccountEntity accountEntity = getAccountEntityByPath(accountHdPath, coin);
if (accountEntity == null) return;
List<AddressEntity> addressEntities = mRepository.loadAddressSync(coin.getCoinId());
Optional<AddressEntity> addressEntityOptional = addressEntities
.stream()
.filter(addressEntity -> addressEntity.getPath()
.startsWith(accountEntity.getHdPath()+"/" + 0))
.max((o1, o2) -> o1.getPath().compareTo(o2.getPath()));
int index = -1;
if (addressEntityOptional.isPresent()) {
try {
AddressIndex addressIndex = CoinPath.parsePath(addressEntityOptional.get().getPath());
index = addressIndex.getValue();
} catch (InvalidPathException e) {
e.printStackTrace();
}
}
if (index < pathIndex) {
final CountDownLatch mLatch = new CountDownLatch(1);
addingAddress.postValue(true);
new AddAddressViewModel.AddAddressTask(coin, mRepository, mLatch::countDown)
.execute(names);
new AddAddressViewModel.AddAddressTask(coin, mRepository, mLatch::countDown,
accountEntity.getExPub(), 0).execute(pathIndex - index);
try {
mLatch.await();
addingAddress.postValue(false);
@ -353,6 +429,22 @@ public class TxConfirmViewModel extends AndroidViewModel {
}
private AccountEntity getAccountEntityByPath(String accountHdPath, CoinEntity coin) {
List<AccountEntity> accountEntities = mRepository.loadAccountsForCoin(coin);
Optional<AccountEntity> optional = accountEntities.stream()
.filter(accountEntity ->
accountEntity.getHdPath().equals(accountHdPath.toUpperCase()))
.findFirst();
AccountEntity accountEntity;
if (optional.isPresent()) {
accountEntity = optional.get();
} else {
return null;
}
return accountEntity;
}
private int getAddressIndex(String hdPath) {
try {
return CoinPath.parsePath(hdPath).getValue();
@ -376,9 +468,45 @@ public class TxConfirmViewModel extends AndroidViewModel {
SignCallback callback = initSignCallback();
signTransaction(transaction, callback, signer);
});
}
public void handleSignPsbt(String psbt) {
AppExecutors.getInstance().diskIO().execute(() -> {
Signer[] signer = initSigners();
SignPsbtCallback callback = new SignPsbtCallback() {
@Override
public void startSign() {
signState.postValue(STATE_SIGNING);
}
@Override
public void onFail() {
signState.postValue(STATE_SIGN_FAIL);
new ClearTokenCallable().call();
}
@Override
public void onSuccess(String txId, String psbtB64) {
TxEntity tx = observableTx.getValue();
Objects.requireNonNull(tx).setTxId(txId);
tx.setSignedHex(psbtB64);
mRepository.insertTx(tx);
signState.postValue(STATE_SIGN_SUCCESS);
new ClearTokenCallable().call();
}
@Override
public void postProgress(int progress) {
}
};
callback.startSign();
Btc btc = new Btc(new BtcImpl());
btc.signPsbt(psbt, callback, signer);
});
}
private SignCallback initSignCallback() {
return new SignCallback() {
@Override
@ -432,34 +560,39 @@ public class TxConfirmViewModel extends AndroidViewModel {
String coinCode = transaction.getCoinCode();
String[] distinctPaths = Stream.of(paths).distinct().toArray(String[]::new);
Signer[] signer = new Signer[distinctPaths.length];
boolean shouldProvidePublicKey = Signer.shouldProvidePublicKey(transaction.getCoinCode());
String exPub = null;
if (shouldProvidePublicKey) {
exPub = mRepository.loadCoinSync(Coins.coinIdFromCoinCode(coinCode)).getExPub();
}
String authToken = getAuthToken();
if (TextUtils.isEmpty(authToken)) {
Log.w(TAG,"authToken null");
return null;
}
CoinEntity coinEntity = mRepository.loadCoinEntityByCoinCode(coinCode);
for (int i = 0; i < distinctPaths.length; i++) {
if (shouldProvidePublicKey) {
String pubKey;
if (Coins.curveFromCoinCode(coinCode) == Coins.CURVE.ED25519) {
pubKey = Util.getPublicKeyHex(exPub).substring(2);
} else {
pubKey = Util.getPublicKeyHex(exPub, distinctPaths[i]);
}
signer[i] = new ChipSigner(distinctPaths[i].toLowerCase(), authToken, pubKey);
} else {
signer[i] = new ChipSigner(distinctPaths[i].toLowerCase(), authToken);
String accountHdPath = getAccountHdPath(distinctPaths[i]);
if (accountHdPath == null) {
return null;
}
AccountEntity accountEntity = getAccountEntityByPath(accountHdPath,coinEntity);
if (accountEntity == null) {
return null;
}
String pubKey = Util.getPublicKeyHex(accountEntity.getExPub(), distinctPaths[i]);
signer[i] = new ChipSigner(distinctPaths[i].toLowerCase(), authToken, pubKey);
}
return signer;
}
private String getAccountHdPath(String addressPath) {
String accountHdPath;
try {
accountHdPath = CoinPath.parsePath(addressPath).getParent().getParent().toString();
} catch (InvalidPathException e) {
e.printStackTrace();
return null;
}
return accountHdPath;
}
private String getAuthToken() {
String authToken = null;
if (!TextUtils.isEmpty(token.password)) {

59
app/src/main/java/com/cobo/cold/viewmodel/WalletInfoViewModel.java

@ -0,0 +1,59 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.viewmodel;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import com.cobo.coinlib.utils.Coins;
import com.cobo.cold.AppExecutors;
import com.cobo.cold.DataRepository;
import com.cobo.cold.MainApplication;
import com.cobo.cold.callables.GetMasterFingerprintCallable;
import com.cobo.cold.db.entity.AccountEntity;
import com.cobo.cold.db.entity.CoinEntity;
public class WalletInfoViewModel extends AndroidViewModel {
private final MutableLiveData<String> fingerprint = new MutableLiveData<>("");
private final MutableLiveData<String> xpub = new MutableLiveData<>("");
public WalletInfoViewModel(@NonNull Application application) {
super(application);
}
public MutableLiveData<String> getFingerprint() {
AppExecutors.getInstance().diskIO().execute(() -> {
String masterFingerprint = new GetMasterFingerprintCallable().call();
fingerprint.postValue(masterFingerprint);
});
return fingerprint;
}
public MutableLiveData<String> getXpub(Coins.Account account) {
AppExecutors.getInstance().diskIO().execute(() -> {
DataRepository repo = ((MainApplication)getApplication()).getRepository();
CoinEntity coinEntity = repo.loadCoinEntityByCoinCode(Coins.BTC.coinCode());
AccountEntity accountEntity = repo.loadAccountsByPath(coinEntity.getId(), account.getPath());
xpub.postValue(accountEntity.getExPub());
});
return xpub;
}
}

24
app/src/main/java/com/cobo/cold/viewmodel/WatchWalletNotMatchException.java

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.cold.viewmodel;
public class WatchWalletNotMatchException extends Exception {
public WatchWalletNotMatchException(String message) {
super(message);
}
}

BIN
app/src/main/res/drawable-xhdpi/electrum.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
app/src/main/res/drawable-xhdpi/sdcard.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

23
app/src/main/res/drawable/text_button_bg.xml

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorAccent" />
<corners android:radius="10.5dp" />
</shape>

83
app/src/main/res/layout/add_address_bottom_sheet.xml

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="value"
type="int" />
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#181717">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="53dp"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_address_num"
android:layout_centerVertical="true"
android:paddingHorizontal="14dp"
android:textColor="@color/white" />
<ImageView
android:id="@+id/close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/close"
android:tint="@color/colorAccent"
android:padding="10dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="6dp" />
</RelativeLayout>
<cn.carbswang.android.numberpickerview.library.NumberPickerView
android:id="@+id/picker"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:npv_ShowDivider="true"
app:npv_DividerColor="#0bffffff"
app:npv_DividerMarginLeft="140dp"
app:npv_DividerMarginRight="140dp"
app:npv_ItemPaddingVertical="10dp"
app:npv_ShownCount="5"
app:npv_TextColorNormal="#90979c"
app:npv_TextColorSelected="@color/white"
app:npv_TextSizeNormal="14sp"
app:npv_TextSizeSelected="14sp"
app:npv_WrapSelectorWheel="false" />
<Button
android:id="@+id/confirm"
android:layout_width="match_parent"
android:layout_margin="16dp"
style="@style/AcceptButton"
android:text="@string/confirm_add_address"/>
</LinearLayout>
</layout>

5
app/src/main/res/layout/address_item.xml

@ -64,7 +64,7 @@
<TextView
android:id="@+id/addr"
android:layout_width="133dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:ellipsize="middle"
@ -87,7 +87,8 @@
android:alpha="0.5"
android:background="?attr/selectableItemBackground"
android:src="@drawable/edit"
tools:ignore="ContentDescription" />
tools:ignore="ContentDescription"
android:visibility="gone"/>
<include
layout="@layout/divider"

90
app/src/main/res/layout/asset_fragment.xml

@ -16,8 +16,7 @@
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
@ -43,81 +42,24 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
app:icon="@{coinViewModel.coin.coinCode}"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="2dp"
android:text="@{coinViewModel.coin.coinCode}"
android:textColor="@android:color/white"
android:textSize="11sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.appcompat.widget.Toolbar>
<LinearLayout
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#000000"
android:visibility="invisible">
<com.cobo.cold.ui.views.MenuHidingEditText
android:id="@+id/search"
android:layout_width="0dp"
android:layout_height="match_parent"
android:textColorHint="@color/white40"
android:textColor="@color/white"
android:textSize="15sp"
android:paddingHorizontal="16dp"
android:background="@null"
android:hint="@string/search"
android:singleLine="true"
android:text="@={query}"
android:layout_weight="1"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:inputType="text" />
<TextView
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:text="@string/cancel"
android:gravity="center"
android:paddingHorizontal="10dp"
android:background="?attr/selectableItemBackground"
android:textColor="@color/colorAccent" />
</LinearLayout>
app:title="Electrum"
app:titleTextColor="@color/white"
app:titleTextAppearance="@style/Toolbar.TitleText"
app:navigationIcon="@drawable/menu"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:menu="@menu/asset_hasmore"
app:contentInsetStartWithNavigation="0dp"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
app:tabTextAppearance="@style/TabTextAppearance"
app:tabIndicatorColor="@color/colorAccent"
app:tabIndicatorFullWidth="false"
app:tabIndicatorHeight="2dp"
app:tabMode="fixed"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/white"
app:tabTextColor="@color/white40" />
@ -132,6 +74,16 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/tab" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp"
android:src="@drawable/plus"
/>
</RelativeLayout>
</layout>

38
app/src/main/res/layout/dialog_bottom_sheet.xml

@ -29,7 +29,7 @@
android:background="#181717">
<RelativeLayout
android:id="@+id/add_address"
android:id="@+id/sign_history"
android:layout_width="match_parent"
android:layout_height="53dp"
android:orientation="horizontal"
@ -38,7 +38,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_address"
android:text="@string/signing_history"
android:layout_centerVertical="true"
android:paddingHorizontal="16dp"
android:textColor="@color/white" />
@ -59,7 +59,7 @@
android:background="#12ffffff" />
<RelativeLayout
android:id="@+id/export_xpub_to_electrum"
android:id="@+id/export_xpub"
android:layout_width="match_parent"
android:layout_height="53dp"
android:orientation="horizontal"
@ -68,7 +68,37 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/export_to_electrum"
android:text="@string/export_wallet"
android:layout_centerVertical="true"
android:paddingHorizontal="16dp"
android:textColor="@color/white" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/arrow_right"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="16dp"
android:background="#12ffffff" />
<RelativeLayout
android:id="@+id/wallet_info"
android:layout_width="match_parent"
android:layout_height="53dp"
android:orientation="horizontal"
android:background="?attr/selectableItemBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wallet_info"
android:layout_centerVertical="true"
android:paddingHorizontal="16dp"
android:textColor="@color/white" />

2
app/src/main/res/layout/electrum_export.xml

@ -129,11 +129,11 @@
android:layout_weight="1" />
<TextView
android:id="@+id/address_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="@string/master_xpub"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />

2
app/src/main/res/layout/electrum_tx_detail.xml

@ -163,6 +163,7 @@
android:textStyle="bold" />
<TextView
android:id="@+id/watch_wallet"
android:layout_width="184dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
@ -349,6 +350,7 @@
android:layout_marginHorizontal="16dp"
android:text="@string/export_signed_txn_file"/>
<TextView
android:id="@+id/broadcast_guide"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="16dp"

155
app/src/main/res/layout/export_xpub.xml

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="20dp"
android:ellipsize="middle"
android:singleLine="true"
android:text="@string/electrum_compatibility"
android:textColor="@android:color/white"
android:textSize="15sp" />
</androidx.appcompat.widget.Toolbar>
<include layout="@layout/divider" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/scan_xpub"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<com.cobo.cold.ui.views.qrcode.QrCodeView
android:id="@+id/qrcode"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginHorizontal="34dp"
android:layout_marginTop="16dp"
android:background="@color/white"
android:keepScreenOn="true"
android:padding="5dp">
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
tools:ignore="ContentDescription" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
</com.cobo.cold.ui.views.qrcode.QrCodeView>
<com.cobo.cold.ui.views.SpanedTextView
android:id="@+id/export_to_sdcard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:paddingVertical="10dp"
android:gravity="center"
android:text="@string/electrum_qrcode_hint"
android:textColor="@color/white"
android:textSize="12sp" />
<androidx.legacy.widget.Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/address_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/expub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:textColor="@color/white40"
android:textSize="15sp"
tools:text="ypub6D3i46Y43SFfjEBYheBK3btYMRm9Cfb8Tt4M5Bv16tArNBw5ATNyJWjdcMyLxoCdHWTvm3ak7j2BWacq5Lw478aYUeARoYm4dvaQgJBAGsb" />
<Button
android:id="@+id/done"
android:layout_width="match_parent"
android:layout_margin="16dp"
style="@style/AcceptButton"
android:text="@string/complete"/>
</LinearLayout>
</ScrollView>
</LinearLayout>
</layout>

16
app/src/main/res/layout/electrum_export_guide.xml → app/src/main/res/layout/export_xpub_guide.xml

@ -51,6 +51,7 @@
<include layout="@layout/divider" />
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
@ -60,7 +61,8 @@
android:textSize="15sp"
android:textStyle="bold" />
<TextView
<com.cobo.cold.ui.views.SpanedTextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingExtra="6dp"
@ -79,8 +81,18 @@
style="@style/AcceptButton"
android:layout_width="match_parent"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="20dp"
android:layout_marginBottom="0dp"
android:text="@string/export_xpub" />
<TextView
android:id="@+id/skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="10dp"
android:textColor="@color/colorAccent"
android:gravity="center"
android:text="@string/skip_export"/>
</LinearLayout>
</layout>

2
app/src/main/res/layout/txn_list.xml → app/src/main/res/layout/file_list.xml

@ -32,7 +32,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/menu"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView

2
app/src/main/res/layout/input_modal.xml

@ -71,7 +71,7 @@
android:textColor="#8F95AA"
tools:ignore="LabelFor"
android:importantForAutofill="no"
android:inputType="text" />
android:inputType="" />
<Button
android:id="@+id/confirm"

10
app/src/main/res/layout/list_preference.xml

@ -52,9 +52,17 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:listitem="@layout/setting_item_selectable"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<Button
android:id="@+id/confirm"
android:layout_width="match_parent"
style="@style/AcceptButton"
android:layout_margin="16dp"
android:visibility="gone"/>
</LinearLayout>
</layout>

97
app/src/main/res/layout/picker_dialog.xml

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="value"
type="int" />
</data>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="263dp"
android:layout_height="319dp"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="30dp"
android:gravity="center"
android:paddingHorizontal="16dp"
android:background="@color/colorAccent"
android:text="@string/text_create_address_main_text"
android:textColor="#4a4a4a" />
<cn.carbswang.android.numberpickerview.library.NumberPickerView
android:id="@+id/picker"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/white"
app:npv_ShowDivider="false"
app:npv_ShownCount="7"
app:npv_TextColorNormal="#90979c"
app:npv_TextColorSelected="#4a4a4a"
app:npv_TextSizeNormal="18sp"
app:npv_TextSizeSelected="23sp"
app:npv_WrapSelectorWheel="false" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:background="@color/colorAccent">
<TextView
android:id="@+id/cancel"
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="@string/cancel"
android:textColor="@color/white"
android:background="?attr/selectableItemBackground"
android:layout_height="match_parent" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/white" />
<TextView
android:id="@+id/confirm"
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="@string/confirm"
android:textColor="@color/white"
android:background="?attr/selectableItemBackground"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</layout>

17
app/src/main/res/layout/receive_fragment.xml

@ -33,6 +33,10 @@
<variable
name="address"
type="String" />
<variable
name="path"
type="String" />
</data>
<LinearLayout
@ -88,12 +92,23 @@
android:textSize="12sp"
tools:text="19zhQ 2rKq Xm5r y1pj Co83 JbBK 5zUr bdKgc" />
<TextView
android:layout_width="148dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_horizontal"
android:lineSpacingExtra="5dp"
android:text="@{'('+path+')'}"
android:textColor="@color/white40"
android:textSize="12sp"
tools:text="m/49'/0'/0'" />
<com.cobo.cold.ui.views.qrcode.QrCodeView
android:id="@+id/qrcode"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginHorizontal="34dp"
android:layout_marginTop="28dp"
android:layout_marginTop="16dp"
android:background="@color/white"
android:padding="5dp"
android:keepScreenOn="true">

23
app/src/main/res/layout/select_address_format.xml

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>

22
app/src/main/res/layout/setting_item_selectable.xml

@ -37,7 +37,8 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_height="wrap_content"
android:minHeight="45dp"
android:background="?attr/selectableItemBackground"
android:onClick="@{()->callback.onSelect(index)}">
@ -46,15 +47,24 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:textColor="@color/white"
android:textSize="15sp"
android:gravity="center"
android:layout_centerVertical="true" />
tools:text="title"/>
<TextView
android:id="@+id/summary"
<com.cobo.cold.ui.views.SpanedTextView
android:id="@+id/sub_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_marginStart="16dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="5dp"
android:textColor="@color/white"
android:textSize="12sp"
android:text="summary"
android:visibility="gone"
tools:text="summary" />
<ImageView
android:layout_width="wrap_content"

36
app/src/main/res/layout/setup_sync.xml → app/src/main/res/layout/setup_watch_wallet.xml

@ -41,17 +41,45 @@
android:layout_marginTop="34dp"
app:step="3" />
<include
android:id="@+id/sync"
layout="@layout/sync"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="17sp"
android:textStyle="bold"
android:layout_marginTop="32dp"
android:text="@string/choose_watch_only_wallet"
/>
<fragment
android:id="@+id/list"
android:name="com.cobo.cold.ui.fragment.setting.ChooseWatchWalletFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/white40"
android:text="@string/switch_watch_only_wallet_guide"
android:layout_gravity="center"
android:layout_margin="16dp"/>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="@+id/complete"
style="@style/AcceptButton"
android:layout_width="match_parent"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
android:text="@string/complete" />

0
app/src/main/res/layout/electrum_tx.xml → app/src/main/res/layout/signed_tx.xml

10
app/src/main/res/layout/sync.xml

@ -41,15 +41,5 @@
<include
android:id="@+id/qrcode_layout"
layout="@layout/dynamic_qrcode" />
<com.cobo.cold.ui.views.SpanedTextView
android:id="@+id/hint2"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/white40"
android:gravity="center"
android:text="@string/sync_hint2" />
</LinearLayout>
</layout>

12
app/src/main/res/layout/sync_fragment.xml

@ -33,7 +33,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/menu"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
@ -53,7 +53,15 @@
layout="@layout/sync"
android:layout_marginTop="40dp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="@+id/complete"
android:layout_width="match_parent"
style="@style/AcceptButton"
android:layout_margin="16dp"
android:text="@string/complete"/>
</LinearLayout>

26
app/src/main/res/layout/tx_list.xml

@ -28,6 +28,32 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/signing_history"
android:textColor="@android:color/white"
android:textSize="15sp" />
</androidx.appcompat.widget.Toolbar>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white40"
android:textSize="12sp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/txid"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"

77
app/src/main/res/layout/tx_list_item.xml

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
@ -17,7 +16,6 @@
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
@ -33,86 +31,31 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="76dp"
android:layout_height="46dp"
android:background="?attr/selectableItemBackground"
android:onClick="@{()->txCallback.onClick(tx)}"
android:orientation="vertical">
<TextView
android:id="@+id/from"
android:layout_width="132dp"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
tools:text="0x4f7d6a…52ce9824" />
android:textSize="15sp"
android:text="@{tx.txId}"
tools:text="adadadadadadadada" />
<ImageView
android:id="@+id/arrow_down"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/from"
android:layout_alignStart="@id/from"
android:layout_marginVertical="8dp"
android:src="@drawable/tx_arrow_down"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/to"
android:layout_width="132dp"
android:layout_height="wrap_content"
android:layout_below="@id/arrow_down"
android:layout_alignStart="@id/arrow_down"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
tools:text="0x4f7d6a…52ce9824" />
<TextView
android:id="@+id/amount"
android:layout_width="125dp"
android:layout_height="wrap_content"
android:src="@drawable/arrow_right"
android:layout_alignParentEnd="true"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:gravity="end"
android:text="@{tx.amount}"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold"
tools:text="-0.15352413 BTC" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/amount"
android:layout_alignEnd="@id/amount"
android:layout_marginTop="4dp"
android:gravity="end"
android:textColor="@color/white40"
android:textSize="12sp"
app:time="@{tx.timeStamp}"
tools:text="2018/06/01" />
<TextView
android:id="@+id/from_wallet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:gravity="end"
android:layout_centerVertical="true"
android:textStyle="bold"
android:textColor="@color/white40"
android:textSize="12sp"
android:visibility="gone"
tools:text="@string/from_electrum" />
android:layout_marginEnd="16dp"/>
<include
layout="@layout/divider"

244
app/src/main/res/layout/wallet_info.xml

@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/arrow_left"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/wallet_info"
android:textColor="@android:color/white"
android:textSize="15sp" />
</androidx.appcompat.widget.Toolbar>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:id="@+id/fingerprint_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:text="@string/wallet"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/fingerprint_label"
android:layout_alignStart="@id/fingerprint_label"
android:layout_marginTop="5dp"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="5271c071" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:id="@+id/address_format_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:text="@string/addressType"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/address_format"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/address_format_label"
android:layout_alignStart="@id/address_format_label"
android:layout_marginTop="5dp"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="@string/nested_segwit" />
<TextView
android:id="@+id/switch_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_toEndOf="@id/address_format"
android:layout_alignTop="@id/address_format"
android:text="@string/switch_address_type"
android:textSize="9sp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textColor="@color/white"
android:background="@drawable/text_button_bg"
/>
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:id="@+id/address_type_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:text="@string/script_type"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/address_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/address_type_label"
android:layout_alignStart="@id/address_type_label"
android:layout_marginTop="5dp"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="P2SH" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp">
<TextView
android:id="@+id/hdpath"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:text="@string/address_path"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/hdpath"
android:layout_alignStart="@id/hdpath"
android:layout_marginTop="5dp"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="M/49'/0'/0'" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="120dp">
<TextView
android:id="@+id/xpub_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:text="@string/xpub"
android:textColor="@color/white40"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/xpub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/xpub_label"
android:layout_alignStart="@id/xpub_label"
android:layout_marginTop="5dp"
android:layout_marginEnd="16dp"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="ypub6WizmVHgZZHMNUtq4T84HzzrTakvdpwUzsmSsFT4ZaRoJpavKYmZweKd1A4aDZz3p51KMPbWwfii9UMnUKvsXgVFdmQrgPYhy7APibMeT3Z" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
android:layout_margin="16dp"
android:textSize="13sp"
android:text="@string/wallet_info_hint"/>
</LinearLayout>
</layout>

55
app/src/main/res/layout/wallet_item.xml

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2020 Cobo
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="?attr/selectableItemBackground">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textColor="@color/white"
android:textSize="15sp"
android:gravity="center"
android:layout_centerVertical="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:src="@drawable/arrow_right"
android:layout_centerVertical="true" />
<include
layout="@layout/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="16dp"
android:layout_alignParentBottom="true" />
</RelativeLayout>
</layout>

12
app/src/main/res/menu/asset_hasmore.xml

@ -19,10 +19,18 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:id="@+id/action_scan"
android:orderInCategory="100"
android:title="Search"
android:icon="@drawable/search"
android:icon="@drawable/scan_qr_code"
android:iconTint="@color/white"
app:showAsAction="always" />
<item
android:id="@+id/action_sdcard"
android:orderInCategory="100"
android:title="Search"
android:icon="@drawable/sdcard"
android:iconTint="@color/white"
app:showAsAction="always" />

229
app/src/main/res/navigation/nav_graph_main.xml

@ -20,48 +20,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph_main.xml"
app:startDestination="@id/assetListFragment">
app:startDestination="@id/assetFragment">
<fragment
android:id="@+id/assetListFragment"
android:name="com.cobo.cold.ui.fragment.main.AssetListFragment"
android:label="AssetListFragment"
tools:layout="@layout/asset_list_fragment">
<action
android:id="@+id/action_to_scan"
app:destination="@id/QRCodeScanFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_to_aboutFragment"
app:destination="@id/aboutFragment" />
<action
android:id="@+id/action_to_settingFragment"
app:destination="@id/settingFragment" />
<action
android:id="@+id/action_to_syncFragment"
app:destination="@id/syncFragment" />
<action
android:id="@+id/action_to_txnListFragment"
app:destination="@id/txnListFragment" />
<action
android:id="@+id/action_to_assetFragment"
app:destination="@id/assetFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@id/action_to_verifyMnemonic"
app:destination="@id/verifyMnemonicFragment1"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/QRCodeScanFragment"
android:name="com.cobo.cold.ui.fragment.main.QRCodeScanFragment"
@ -74,7 +34,7 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/assetListFragment"
app:popUpTo="@id/assetFragment"
app:popUpToInclusive="false" />
<action
@ -84,7 +44,7 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"
app:popUpTo="@id/assetListFragment"
app:popUpTo="@id/assetFragment"
app:popUpToInclusive="false" />
<action
@ -98,7 +58,7 @@
tools:layout="@layout/pattern_lock_fragment">
<action
android:id="@+id/action_unlock_to_assetList"
app:destination="@id/assetListFragment" />
app:destination="@id/assetFragment" />
</fragment>
<fragment
android:id="@+id/aboutFragment"
@ -192,7 +152,18 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_to_chooseWatchOnly"
app:destination="@id/chooseWatchOnly"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/chooseWatchOnly"
android:name="com.cobo.cold.ui.fragment.setting.ChooseWatchWalletFragment"
android:label="ChooseWatchOnlyFragment" />
<fragment
android:id="@+id/syncFragment"
android:name="com.cobo.cold.ui.fragment.SyncFragment"
@ -202,7 +173,7 @@
<fragment
android:id="@+id/txnListFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumTxnListFragment"
tools:layout="@layout/txn_list"
tools:layout="@layout/file_list"
android:label="ElectrumTxnListFragment" >
<action
android:id="@id/action_to_ElectrumTxConfirmFragment"
@ -238,6 +209,114 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@+id/action_to_export_xpub_guide"
app:destination="@id/exportXpubGuideFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@+id/action_to_export_xpub_generic"
app:destination="@id/exportGenericXpubFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@+id/action_to_txList"
app:destination="@id/txListFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_to_scan"
app:destination="@id/QRCodeScanFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_to_aboutFragment"
app:destination="@id/aboutFragment" />
<action
android:id="@+id/action_to_settingFragment"
app:destination="@id/settingFragment" />
<action
android:id="@+id/action_to_txnListFragment"
app:destination="@id/txnListFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"/>
<action
android:id="@+id/action_to_psbtListFragment"
app:destination="@id/psbtListFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"/>
<action
android:id="@id/action_to_verifyMnemonic"
app:destination="@id/verifyMnemonicFragment1"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_to_walletInfoFragment"
app:destination="@id/walletInfoFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/exportGenericXpubFragment"
android:name="com.cobo.cold.ui.fragment.main.ExportGenericXpubFragment"
android:label="PsbtListFragment"/>
<fragment
android:id="@+id/psbtListFragment"
android:name="com.cobo.cold.ui.fragment.main.PsbtListFragment"
android:label="PsbtListFragment">
<action
android:id="@+id/action_to_psbtTxConfirmFragment"
app:destination="@id/psbtTxConfirmFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"
/>
</fragment>
<fragment
android:id="@+id/psbtTxConfirmFragment"
android:name="com.cobo.cold.ui.fragment.main.PsbtTxConfirmFragment"
android:label="PsbtTxConfirmFragment"/>
<fragment
android:id="@+id/walletInfoFragment"
android:name="com.cobo.cold.ui.fragment.main.WalletInfoFragment"
tools:layout="@layout/wallet_info"
android:label="WalletInfoFragment">
<action
android:id="@+id/action_to_selectAddressFormatFragment"
app:destination="@id/selectAddressFormatFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@id/selectAddressFormatFragment"
android:name="com.cobo.cold.ui.fragment.setup.SelectAddressFormatFragment"
android:label="SelectAddressFormatFragment">
</fragment>
<fragment
android:id="@+id/txListFragment"
android:name="com.cobo.cold.ui.fragment.main.TxListFragment"
tools:layout="@layout/export_xpub_guide"
android:label="TxListFragment">
<action
android:id="@+id/action_to_electrumTxFragment"
app:destination="@id/electrumTxFragment"
@ -245,17 +324,32 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@+id/action_to_electrum_guide"
app:destination="@id/electrumGuideFragment"
<action
android:id="@+id/action_to_psbtSignedTxFragment"
app:destination="@id/psbtSignedTxFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@id/action_to_txFragment"
app:destination="@id/txFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/psbtSignedTxFragment"
android:name="com.cobo.cold.ui.fragment.main.PsbtSignedTxFragment"
android:label="PsbtSignedTxFragment">
</fragment>
<fragment
android:id="@+id/electrumGuideFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumGuideFragment"
tools:layout="@layout/electrum_export_guide"
android:id="@+id/exportXpubGuideFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ExportXpubGuideFragment"
tools:layout="@layout/export_xpub_guide"
android:label="ElectrumGuideFragment">
<action android:id="@+id/export_electrum_ypub"
app:destination="@id/electrumExportFragment"
@ -263,6 +357,13 @@
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@+id/export_xpub_cobo"
app:destination="@id/syncFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
@ -282,8 +383,8 @@
<action
android:id="@id/action_to_home"
app:destination="@id/assetListFragment"
app:popUpTo="@id/assetListFragment"
app:destination="@id/assetFragment"
app:popUpTo="@id/assetFragment"
app:popUpToInclusive="false"
app:launchSingleTop="true" />
<action
@ -298,7 +399,7 @@
<fragment
android:id="@+id/electrumTxConfirmFragment"
tools:layout="@layout/tx_confirm_fragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumTxConfirmFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.UnsignedTxFragment"
android:label="TxConfirmFragment">
<action
android:id="@+id/action_to_broadcastElectrumTxFragment"
@ -310,8 +411,8 @@
<action
android:id="@id/action_to_home"
app:destination="@id/assetListFragment"
app:popUpTo="@id/assetListFragment"
app:destination="@id/assetFragment"
app:popUpTo="@id/assetFragment"
app:popUpToInclusive="false"
app:launchSingleTop="true" />
<action
@ -330,8 +431,8 @@
android:label="BroadcastTxFragment">
<action
android:id="@+id/action_to_home"
app:destination="@id/assetListFragment"
app:popUpTo="@id/assetListFragment"
app:destination="@id/assetFragment"
app:popUpTo="@id/assetFragment"
app:popUpToInclusive="false"
app:launchSingleTop="true" />
</fragment>
@ -343,8 +444,8 @@
android:label="ElectrumBroadcastTxFragment">
<action
android:id="@+id/action_to_home"
app:destination="@id/assetListFragment"
app:popUpTo="@id/assetListFragment"
app:destination="@id/assetFragment"
app:popUpTo="@id/assetFragment"
app:popUpToInclusive="false"
app:launchSingleTop="true" />
</fragment>
@ -455,8 +556,8 @@
android:label="TxFragment" />
<fragment
android:id="@+id/electrumTxFragment"
tools:layout="@layout/electrum_tx"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumTxFragment"
tools:layout="@layout/signed_tx"
android:name="com.cobo.cold.ui.fragment.main.electrum.SignedTxFragment"
android:label="ElectrumTxFragment" />
<fragment
android:id="@+id/setPatternUnlockFragment"
@ -481,8 +582,8 @@
android:label="WebAuthResultFragment">
<action
android:id="@+id/action_auth_to_home"
app:destination="@id/assetListFragment"
app:popUpTo="@id/assetListFragment"
app:destination="@id/assetFragment"
app:popUpTo="@id/assetFragment"
app:popUpToInclusive="false"
app:launchSingleTop="true" />
</fragment>
@ -526,7 +627,7 @@
tools:layout="@layout/passphrase">
<action
android:id="@+id/to_assetListFragment"
app:destination="@id/assetListFragment" />
app:destination="@id/assetFragment" />
</fragment>
</navigation>

95
app/src/main/res/navigation/nav_graph_setup.xml

@ -80,7 +80,7 @@
tools:layout="@layout/mnemonic_input_fragment">
<action
android:id="@id/action_to_setupSyncFragment"
app:destination="@id/setupSyncFragment" />
app:destination="@id/SetupWatchWalletFragment" />
</fragment>
<fragment
android:id="@+id/setPasswordFragment"
@ -123,16 +123,29 @@
tools:layout="@layout/mnemonic_input_fragment">
<action
android:id="@+id/action_to_setupSyncFragment"
app:destination="@id/setupSyncFragment" />
app:destination="@id/SetupWatchWalletFragment" />
</fragment>
<fragment
android:id="@+id/setupSyncFragment"
tools:layout="@layout/setup_sync"
android:name="com.cobo.cold.ui.fragment.setup.SetupSyncFragment"
android:label="SetupSyncFragment">
android:id="@+id/SetupWatchWalletFragment"
tools:layout="@layout/setup_watch_wallet"
android:name="com.cobo.cold.ui.fragment.setup.SetupWatchWalletFragment"
android:label="SetupWatchWalletFragment">
<action
android:id="@+id/action_to_selectAddressFormatFragment"
app:destination="@id/selectAddressFormatFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"/>
<action
android:id="@+id/action_to_setupCompleteFragment"
app:destination="@id/setupCompleteFragment" />
android:id="@id/action_to_export_xpub_guide"
app:destination="@id/exportXpubGuideFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"/>
</fragment>
<fragment
android:id="@+id/setupCompleteFragment"
@ -166,7 +179,7 @@
app:popExitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_setupManageWhiteList_to_setupSyncFragment"
app:destination="@id/setupSyncFragment" />
app:destination="@id/SetupWatchWalletFragment" />
</fragment>
<fragment
@ -209,4 +222,68 @@
app:popExitAnim="@android:anim/slide_out_right"/>
</fragment>
<fragment
android:id="@+id/selectAddressFormatFragment"
android:name="com.cobo.cold.ui.fragment.setup.SelectAddressFormatFragment"
android:label="SelectAddressFormatFragment">
<action
android:id="@id/action_to_export_xpub_guide"
app:destination="@id/exportXpubGuideFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right"/>
</fragment>
<fragment
android:id="@id/exportXpubGuideFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ExportXpubGuideFragment"
tools:layout="@layout/export_xpub_guide"
android:label="ElectrumGuideFragment">
<action android:id="@id/action_to_setupCompleteFragment"
app:destination="@id/setupCompleteFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@id/export_electrum_ypub"
app:destination="@id/electrumExportFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
<action android:id="@+id/export_xpub_cobo"
app:destination="@id/syncFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@id/syncFragment"
android:name="com.cobo.cold.ui.fragment.SyncFragment"
tools:layout="@layout/sync_fragment"
android:label="SyncFragment" >
<action android:id="@id/action_to_setupCompleteFragment"
app:destination="@id/setupCompleteFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
<fragment
android:id="@id/electrumExportFragment"
android:name="com.cobo.cold.ui.fragment.main.electrum.ElectrumExportFragment"
tools:layout="@layout/electrum_export"
android:label="ElectrumExportFragment">
<action android:id="@+id/action_to_setupCompleteFragment"
app:destination="@id/setupCompleteFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@android:anim/slide_in_left"
app:popExitAnim="@android:anim/slide_out_right" />
</fragment>
</navigation>

91
app/src/main/res/values-zh-rCN/strings.xml

@ -44,7 +44,8 @@
<string name="mnemonic_hint1">助记词用于恢复钱包,请使用cryptosteel依次记录您的24个助记词,并保存到安全的地方。</string>
<string name="mnemonic_hint2">请勿进行拍照等操作!如果他人获取您的助记词将直接获取您的资产!</string>
<string name="saved">我已安全保存</string>
<string name="tab_my_address">我的地址</string>
<string name="tab_my_address">接收地址</string>
<string name="tab_my_change_address">找零地址</string>
<string name="tab_my_pubkey">我的公钥</string>
<string name="tab_transaction_history">签名记录</string>
<string name="create_addr">创建 %s 地址</string>
@ -106,7 +107,7 @@
</string-array>
<string name="sync_hint2"><![CDATA[Cobo 金库移动端可在 Cobo 官网下载:<br><font color="#00cdc3"><u>https://cobo.com/hardware-wallet/cobo-vault-app</u><font/><br>]]></string>
<string name="sync_hint1"><![CDATA[请使用 Cobo 金库移动端扫描<font color="#00cdc3">动态二维码<font/>]]></string>
<string name="sync_hint1"><![CDATA[请使用 Cobo 金库移动端扫描二维码]]></string>
<string name="wrong_mnemonic_please_check">助记词有误,请逐字检查</string>
<string name="dots_not_enough">请至少连接四个点</string>
<string name="input_pattern_again">请再次绘制解锁图案</string>
@ -267,7 +268,9 @@
<item>"写入安全芯片…"</item>
</string-array>
<string name="export_to_electrum">在 Electrum 创建观察钱包</string>
<string name="export_to_electrum_guide">1. 打开 Electrum,新建钱包 \n2. 选择“标准钱包” \n3. 选择“使用主公钥” \n4. 在输入主公钥页面点击照相机按钮,扫描主公钥二维码(点击下方按钮查看二维码)\n5. 设置钱包密码 \n6. 进入观察钱包</string>
<string name="export_to_electrum_guide"><![CDATA[
1. 打开 Electrum,新建钱包 <br>2. 选择“标准钱包” <br>3. 选择“使用主公钥” <br>4. 在输入主公钥页面点击照相机按钮,扫描主公钥二维码(点击下方按钮查看二维码)<br>5. 设置钱包密码 <br>6. 进入观察钱包
]]></string>
<string name="export_xpub">查看主公钥二维码</string>
<string name="export_to_electrum_guide_hint">Electrum 创建指引</string>
<string name="send_address">发送地址 %d</string>
@ -294,12 +297,13 @@
<string name="transaction_from_electrum">该笔交易热端来源于Electrum</string>
<string name="add_address">添加地址</string>
<string name="use_electrum_scan_xpub">请用 Electrum 扫描主公钥二维码</string>
<string name="scan_xpub">请扫描主公钥二维码</string>
<string name="export_signed_txn_file">导出已签名文件</string>
<string name="export_signed_txn_action_guide">导出后,用 Electrum 进行广播\n前往【Electrum--工具--加载交易--从文件】打开该文件</string>
<string name="export">导出</string>
<string name="export_xpub_text_file">导出主公钥文件</string>
<string name="file_name_label">该文件的文件名为:</string>
<string name="master_xpub">主公钥(隔离见证兼容</string>
<string name="master_xpub">主公钥(%s</string>
<string name="electrum_qrcode_hint"><![CDATA[扫码困难?点击二维码进行放大<br>或<u>\点击此处导出文件\</u>]]></string>
<string name="scan_electrum_hint">若扫描 Electrum 困难,可将待签交易导出成文件,前往【菜单--读取TF卡】对文件签名。</string>
<string name="sign_txn_in_sdcard">对文件签名</string>
@ -320,4 +324,83 @@
<string name="dice_roll_guide"><![CDATA[请注意:<br><br> 1. 至少投掷50次,才能生成助记词;建议您最少投掷99次,获得256位的随机熵值;Cobo 金库不限制您的最大投掷次数。<br>2. 建议您使用<font color="#00cdc3">赌场级骰子<font/>(六个面向上的概率相同),操作过程中确保骰子在空中得到足够的翻转,且避免总是从骰子特定面朝上进行投掷。]]></string>
<string name="start">开始</string>
<string name="pattern_lock_hide_track">为防偷窥,解锁时将隐藏轨迹</string>
<string name="select_address_format">选择地址格式</string>
<string name="select_address_num">选择数量(%s)</string>
<string name="change_address">找零地址</string>
<string name="export_xpub_guide_title_electrum">导出到Electrum</string>
<string name="export_xpub_guide_title_wasabi">导出到Wasabi Wallet</string>
<string name="export_xpub_guide_title_cobo">导出到Cobo金库移动端</string>
<string name="export_xpub_guide_title_blue">导出到BlueWallet</string>
<string name="export_xpub_guide_text1_electrum">Electrum 导入指引:</string>
<string name="export_xpub_guide_text1_cobo">Cobo 金库移动端导入指引:</string>
<string name="export_xpub_guide_text1_wasabi">Wasabi 导入指引:</string>
<string name="export_xpub_guide_text1_blue">BlueWallet 导入指引:</string>
<string name="export_xpub_guide_text2_electrum"><![CDATA[
1. 打开 Electrum,新建钱包 <br>
2. 选择<font color="#00cdc3">“标准钱包”<font/> <br>
3. 选择<font color="#00cdc3">“使用主公钥”<font/> <br>
4. 在输入主公钥页面点击照相机按钮,扫描主公钥二维码(点击下方按钮查看二维码)<br>
5. 设置钱包密码 <br>
6. 进入观察钱包
]]></string>
<string name="export_xpub_guide_text2_cobo"><![CDATA[
1. 打开 Cobo 金库移动端,添加新钱包 <br>
2. 选择<font color="#00cdc3">“硬件钱包”<font/>,点击绑定按钮<font/> <br>
3. 勾选隐私协议和服务条款后,点击确定按钮,扫描钱包二维码(点击下方按钮查看) <br><br>
Cobo 金库移动端可在 Cobo 官网下载:<br>
<font color="#00cdc3">https://cobo.com/hardware-wallet/downloads<font/>
]]></string>
<string name="export_xpub_guide_text2_wasabi"><![CDATA[
1. 打开Wasabi,创建新钱包 <br>
2. 选择<font color="#00cdc3">“Hardware Wallet”<font/> <br>
3. 选择<font color="#00cdc3">“Import CoboVault”<font/> <br>
4. 打开 Cobo 金库导出的钱包文件(点击下方按钮导出)<br>
5. 点击<font color="#00cdc3">“Load Wallet”<font/> <br>
6. 加载完成后即可进入观察钱包<br><br>
请确保 Cobo 金库装有TF卡后再进行导出(仅支持FAT32格式,容量不能超过32G)
]]></string>
<string name="export_xpub_guide_text2_blue"><![CDATA[
1. 打开BlueWallet,添加新钱包<br>
2. 选择“导入 Cobo 金库”<br>
3. 扫描 Cobo 金库二维码(点击下方按钮查看)<br>
]]></string>
<string name="show_master_public_key_qrcode">查看主公钥二维码</string>
<string name="show_qrcode">查看二维码</string>
<string name="export_wallet">导出钱包</string>
<string name="native_segwit">隔离见证原生</string>
<string name="p2pkh">支付给公钥哈希</string>
<string name="nested_segwit">隔离见证兼容</string>
<string name="txid">交易ID</string>
<string name="skip_export">暂不导出</string>
<string name="choose_watch_only_wallet">选择观察钱包</string>
<string name="switch_watch_only_wallet_guide">切换观察钱包可前往【菜单--设置--选择观察钱包】</string>
<string name="wallet_info">钱包信息</string>
<string name="wallet">钱包:</string>
<string name="addressType">地址类型:</string>
<string name="switch_address_type">切换地址类型</string>
<string name="script_type">脚本类型:</string>
<string name="address_path">地址路径:</string>
<string name="xpub">扩展公钥:</string>
<string name="wallet_info_hint">*请不要随意泄露该信息(可通过 Public Key 获取钱包地址及知道地址上存有的资产)</string>
<string name="confirm_add_address">确认创建</string>
<string name="watch_wallet_not_match">请使用%s生成待签交易</string>
<string-array name="watch_wallet_list">
<item>Cobo 金库 App</item>
<item>Electrum</item>
<item>Wasabi Wallet</item>
<item>BlueWallet</item>
<item>通用钱包</item>
</string-array>
<string-array name="address_format">
<item>隔离见证兼容(P2WPKH-P2SH)</item>
<item>隔离见证原生(P2WPKH)</item>
</string-array>
<string-array name="address_format_subtitle">
<item><![CDATA[举例:<font color="#00cdc3">3<font/>kgdahdkhghahdgkhaldshg]]></item>
<item><![CDATA[举例:<font color="#00cdc3">bc1<font/>akjhdfhkashdhdhsaghlh]]></item>
</string-array>
</resources>

113
app/src/main/res/values/strings.xml

@ -79,6 +79,38 @@
<item>en</item>
</string-array>
<string-array name="watch_wallet_list">
<item>Cobo Vault App</item>
<item>Electrum</item>
<item>Wasabi Wallet</item>
<item>BlueWallet</item>
<item>Generic Wallet</item>
</string-array>
<string-array name="watch_wallet_list_values" translatable="false">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>
<string-array name="address_format">
<item>Nested SegWit(P2WPKH-P2SH)</item>
<item>Native Segwit(P2WPKH)</item>
</string-array>
<string-array name="address_format_subtitle" translatable="false">
<item><![CDATA[eg. <font color="#00cdc3">3<font/>kgdahdkhghahdgkhaldshg]]></item>
<item><![CDATA[eg. <font color="#00cdc3">bc1<font/>akjhdfhkashdhdhsaghlh]]></item>
</string-array>
<string-array name="address_format_value" translatable="false">
<item>P2WPKH-P2SH</item>
<item>P2WPKH</item>
</string-array>
<string-array name="screen_off_entries">
<item>15 secs</item>
<item>30 secs</item>
@ -97,7 +129,8 @@
<item>600000</item>
</string-array>
<string name="tab_my_address">MY ADDRESSES</string>
<string name="tab_my_address">Receiving</string>
<string name="tab_my_change_address">Change</string>
<string name="web_auth1"><![CDATA[1. Please visit <font color="#00cdc3">https://cobo.com/hardware-wallet/web-authentication<font/>]]></string>
<string name="web_auth2"><![CDATA[2. Click on <font color="#00cdc3">\"Scan QR Code\" <font/>below and scan the QR code displayed on the website]]></string>
<string name="web_auth3">3. Enter the code displayed on your Cobo Vault on the official website for authentication;</string>
@ -126,7 +159,7 @@
<string name="complete">Done</string>
<string name="password_verify_too_short">Too short</string>
<string name="sync_hint2"><![CDATA[Download Cobo Vault Mobile:<br><font color="#00cdc3"><u>https://cobo.com/hardware-wallet/cobo-vault-app</u><font/><br>]]></string>
<string name="sync_hint1"><![CDATA[Scan <font color="#00cdc3">QR Code<font/> with Cobo Vault Mobile]]></string>
<string name="sync_hint1"><![CDATA[Scan QR Code with Cobo Vault Mobile]]></string>
<string name="wrong_mnemonic_please_check">recovery phrase incorrect, please try again.</string>
<string name="dots_not_enough">Please connect at least 4 dots.</string>
<string name="input_pattern_again">Please draw again to confirm.</string>
@ -168,7 +201,7 @@
<string name="Update">Update</string>
<string name="draw_pattern_again">Please draw again to confirm</string>
<string name="scan_to_auth">Scan QR Code</string>
<string name="signing_history">Signing Details</string>
<string name="signing_history">Signatures</string>
<string name="add_coins">Add Cryptocurrency</string>
<string name="not_in_whitelist_reject">Receiving address unauthorized, transaction declined!</string>
<string name="invalid_mnemonic">recovery phrase incorrect, please check</string>
@ -290,8 +323,10 @@
<item>"Writing Secure Element…"</item>
</string-array>
<string name="export_to_electrum">Create Watch-Only Wallet in Electrum</string>
<string name="export_to_electrum_guide">1. Open Electrum and create a new wallet. \n2. Choose “Standard wallet”. \n3. Choose “Use a master key”. \n4. Touch “Show Master Public Key” below then click the camera icon in the “Create keystore from a master key” window in Electrum to scan the QR code that displays on Cobo Vault.
\n5. Set a password. \n6. Enter the watch-only wallet.</string>
<string name="export_to_electrum_guide"><![CDATA[
1. Open Electrum and create a new wallet. <br>2. Choose “Standard wallet”. <br>3. Choose “Use a master key”. <br>4. Touch “Show Master Public Key” below then click the camera icon in the “Create keystore from a master key” window in Electrum to scan the QR code that displays on Cobo Vault.
<br>5. Set a password. <br>6. Enter the watch-only wallet.
]]></string>
<string name="export_xpub">Show Master Public Key</string>
<string name="export_to_electrum_guide_hint">How to create a watch-only wallet in Electrum:</string>
<string name="send_address">From %d</string>
@ -318,12 +353,13 @@
<string name="transaction_from_electrum">This transaction is from Electrum</string>
<string name="add_address">Add Address</string>
<string name="use_electrum_scan_xpub">Scan master public key using Electrum</string>
<string name="scan_xpub">Scan master public key</string>
<string name="export_signed_txn_file">Export Signed File</string>
<string name="export_signed_txn_action_guide">How to broadcast with Electrum:\nIn Electrum, go to Tools > Load Transaction > From File. Open the file, and click “Broadcast”.</string>
<string name="export">Export</string>
<string name="export_xpub_text_file">Export Master Public Key File</string>
<string name="file_name_label">File name:</string>
<string name="master_xpub">Master Public Key (Nested SegWit)</string>
<string name="master_xpub">Master Public Key (%s)</string>
<string name="electrum_qrcode_hint"><![CDATA[Difficulty scanning?Tap the QR code to enlarge<br>or <u>touch here to export via microSD.</u>]]></string>
<string name="scan_electrum_hint">Difficulty scanning? You can import pending transactions from Electrum using a microSD card (Menu > MicroSD Card).</string>
<string name="sign_txn_in_sdcard">Unsigned Transaction Files</string>
@ -337,11 +373,74 @@
<string name="only_support_btc">Please scan only BTC</string>
<string name="keep_rolling">Keep it Rolling</string>
<string name="confirm_rolling">Generate</string>
<string name="rolling_hint_less_than_99">You have rolled %d times. We recommend you roll 99 times for 256-bit <entropy class=""></entropy></string>
<string name="rolling_hint_less_than_99">You have rolled %d times. We recommend you roll 99 times for 256-bit </string>
<string name="rolling_hint_less_than_50">You have rolled %d times. You need at least 50 rolls to generate a recovery phrase.</string>
<string name="rolling_not_enough">Keep it Rolling</string>
<string name="dice_rolls">Dice Rolls</string>
<string name="dice_roll_guide"><![CDATA[How to use dice to generate keys:<br><br> 1. Roll the dice at least 50 times. Roll 99 times to obtain 256-bit entropy (recommended). There is no limit to the number of rolls you can use.<br>2. We recommend you use <font color="#00cdc3">casino dice<font/> to increase the entropy of each dice throw.]]></string>
<string name="start">Start</string>
<string name="pattern_lock_hide_track">To prevent peeping, track will be hidden when unlocked</string>
<string name="select_address_format">Toggle Address Type</string>
<string name="select_address_num">Select number(%s)</string>
<string name="change_address">Change Address</string>
<string name="export_xpub_guide_title_electrum">Export Electrum Watch-Only Wallet</string>
<string name="export_xpub_guide_title_wasabi">Export Wasabi Watch-Only Wallet</string>
<string name="export_xpub_guide_title_cobo">Export Cobo Vault Mobile Watch-Only Wallet</string>
<string name="export_xpub_guide_title_blue">Export BlueWallet Watch-Only Wallet</string>
<string name="export_xpub_guide_text1_electrum">How to create a watch-only wallet in BlueWallet:</string>
<string name="export_xpub_guide_text1_cobo">How to create a watch-only wallet in Cobo Vault Mobile:</string>
<string name="export_xpub_guide_text1_wasabi">How to create a watch-only wallet in Wasabi:</string>
<string name="export_xpub_guide_text1_blue">How to create a watch-only wallet in BlueWallet:</string>
<string name="export_xpub_guide_text2_electrum"><![CDATA[
1. Open Electrum and create a new wallet. <br>
2. Choose <font color="#00cdc3">“Standard wallet”<font/>. <br>
3. Choose <font color="#00cdc3">“Use a master key”<font/>. <br>
4. Touch “Display Master Public Key QR Code” below then click the camera icon in Electrum to scan the QR code that displays on Cobo Vault.<br>
5. Set a password. <br>
6. Enter the watch-only wallet.
]]></string>
<string name="export_xpub_guide_text2_cobo"><![CDATA[
1. Open Cobo Vault Mobile and add new wallet. <br>
2. Choose <font color="#00cdc3">“Hardware Wallet”<font/>, and touch "Bind" button. <br>
3. Touch <font color="#00cdc3">“Display Master Public Key QR Code”<font/> below then scan the OR code with Cobo Vault App. <br><br>
Download Cobo Vault Mobile App:<br>
<font color="#00cdc3">https://cobo.com/hardware-wallet/downloads<font/>
]]></string>
<string name="export_xpub_guide_text2_wasabi"><![CDATA[
1. open Wasabi and create a new wallet <br>
2. Choose <font color="#00cdc3">“Hardware Wallet”<font/> <br>
3. Choose <font color="#00cdc3">“Import CoboVault”<font/> <br>
4. Touch <font color="#00cdc3">"Export Wallet"<font/> below then open the file in Wasabi <br>
5. Click <font color="#00cdc3">“Load Wallet”<font/> <br>
6. After loading, you can enter the watch-only wallet<br><br>
Please make sure you have inserted a FAT32 format microSD card with capacity 32GB or less before exporting
]]></string>
<string name="export_xpub_guide_text2_blue"><![CDATA[
1. Open BlueWallet,add wallet.<br>
2. Choose “Import Cobo Vault”.
3. Touch <font color="#00cdc3">“Show QR code”<font/> and scan the QR code with BlueWallet.<br>
]]></string>
<string name="show_master_public_key_qrcode">Display Master Public Key QR Code</string>
<string name="show_qrcode">Display QR Code</string>
<string name="export_wallet">Export Wallet</string>
<string name="native_segwit">Native Segwit</string>
<string name="p2pkh">Pay To Public Key Hash</string>
<string name="nested_segwit">Nested SegWit</string>
<string name="txid">TxID</string>
<string name="skip_export">Export Later</string>
<string name="choose_watch_only_wallet">Choose Watch Wallet</string>
<string name="switch_watch_only_wallet_guide">Go to Menu > Setting > Watch-only wallet to switch</string>
<string name="wallet_info">Wallet Info</string>
<string name="wallet">Key Fingerprint:</string>
<string name="addressType">Address Type:</string>
<string name="switch_address_type">Toggle Address Type</string>
<string name="script_type">Script Type:</string>
<string name="address_path">Account Key Path:</string>
<string name="xpub">Account Extended Public Key:</string>
<string name="wallet_info_hint">*Do not share Public Key with others, Account Extended Public Key can get your addresses and find out how may coins you have</string>
<string name="confirm_add_address">Confirm</string>
<string name="watch_wallet_not_match">Please use %s generate unsigned transaction</string>
</resources>

7
app/src/main/res/values/styles.xml

@ -90,4 +90,11 @@
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">false</item>
</style>
<style name="Toolbar.TitleText" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">15sp</item>
</style>
<style name="TabTextAppearance" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</item>
</style>
</resources>

3
app/src/main/res/xml/main_preference.xml

@ -18,6 +18,9 @@
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.cobo.cold.ui.preference.SimplePreference
android:key="setting_choose_watch_only_wallet"
android:title="@string/choose_watch_only_wallet" />
<com.cobo.cold.ui.preference.SimplePreference
android:key="setting_change_pwd"
android:title="@string/change_password" />

10
coinlib/src/main/java/com/cobo/coinlib/Util.java

@ -204,4 +204,14 @@ public class Util {
System.arraycopy(checksum, 0, result, bytes.length, 4);
return Base58.encode(result);
}
public static String convertXpubToZpub(String xpub) {
byte[] bytes = Base58.decodeChecked(xpub);
byte[] result = new byte[bytes.length + 4];
System.arraycopy(int2bytes(0x04b24746), 0, bytes, 0, 4);
byte[] checksum = Sha256Hash.hashTwice(bytes, 0, bytes.length);
System.arraycopy(bytes, 0, result, 0, bytes.length);
System.arraycopy(checksum, 0, result, bytes.length, 4);
return Base58.encode(result);
}
}

53
coinlib/src/main/java/com/cobo/coinlib/coins/AbsDeriver.java

@ -1,53 +0,0 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.coinlib.coins;
import androidx.annotation.NonNull;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDKeyDerivation;
import org.bitcoinj.params.MainNetParams;
public abstract class AbsDeriver {
public static AbsDeriver newInstance(@NonNull String coinCode) {
try {
Class clazz = Class.forName(CoinReflect.getCoinClassByCoinCode(coinCode) + "$Deriver");
return (AbsDeriver) clazz.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
return null;
}
protected static final NetworkParameters MAINNET = MainNetParams.get();
protected DeterministicKey getAddrDeterministicKey(String accountXpub, int changeIndex, int addressIndex) {
DeterministicKey account = DeterministicKey.deserializeB58(accountXpub, MAINNET);
DeterministicKey change = HDKeyDerivation.deriveChildKey(account, changeIndex);
return HDKeyDerivation.deriveChildKey(change, addressIndex);
}
protected DeterministicKey getDeterministicKey(String xPub) {
return DeterministicKey.deserializeB58(xPub, MAINNET);
}
public abstract String derive(String xPubKey, int changeIndex, int addrIndex);
public abstract String derive(String xPubKey);
}

5
coinlib/src/main/java/com/cobo/coinlib/coins/AbsTx.java

@ -81,10 +81,6 @@ public abstract class AbsTx {
if (!coinCode.equals(Coins.coinCodeOfIndex(coinType.getValue()))) {
throw new InvalidTransactionException("invalid hdPath, error coinIndex");
}
if (Coins.purposeNumber(coinCode) != coinType.getParent().getValue()) {
throw new InvalidTransactionException("invalid hdPath, error purpose number");
}
} catch (InvalidPathException e) {
e.printStackTrace();
throw new InvalidTransactionException("invalid hdPath");
@ -94,7 +90,6 @@ public abstract class AbsTx {
public static AbsTx newInstance(JSONObject object) throws JSONException {
String coinCode = object.getString("coinCode");
try {
Class<?> clazz = Class.forName(CoinReflect.getCoinClassByCoinCode(coinCode) + "$Tx");
return (AbsTx) clazz.getDeclaredConstructor(JSONObject.class, String.class)

54
coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Btc.java

@ -20,19 +20,16 @@ package com.cobo.coinlib.coins.BTC;
import androidx.annotation.NonNull;
import com.cobo.coinlib.coins.AbsCoin;
import com.cobo.coinlib.coins.AbsDeriver;
import com.cobo.coinlib.coins.AbsTx;
import com.cobo.coinlib.coins.SignPsbtResult;
import com.cobo.coinlib.coins.SignTxResult;
import com.cobo.coinlib.exception.InvalidTransactionException;
import com.cobo.coinlib.interfaces.Coin;
import com.cobo.coinlib.interfaces.SignCallback;
import com.cobo.coinlib.interfaces.SignPsbtCallback;
import com.cobo.coinlib.interfaces.Signer;
import com.cobo.coinlib.utils.Coins;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -58,6 +55,23 @@ public class Btc extends AbsCoin {
}
}
public JSONObject parsePsbt(@NonNull String psbt) {
return ((BtcImpl)impl).parsePsbt(psbt);
}
public void signPsbt(@NonNull String psbt, SignPsbtCallback callback, Signer... signers) {
if (signers == null) {
callback.onFail();
return;
}
SignPsbtResult result = ((BtcImpl)impl).signPsbt(psbt, signers);
if (result != null && result.isValid()) {
callback.onSuccess(result.txId, result.psbtB64);
} else {
callback.onFail();
}
}
public static class Tx extends AbsTx implements UtxoTx {
protected long inputAmount;
@ -179,6 +193,7 @@ public class Btc extends AbsCoin {
for (int i = 0; i < inputs.length(); i++) {
JSONObject input = inputs.getJSONObject(i);
String path = input.getString("ownerKeyPath");
input.put("bip32Derivation",new JSONArray());
checkHdPath(path, false);
paths.append(path).append(SEPARATOR);
int index = input.optInt("index");
@ -206,30 +221,11 @@ public class Btc extends AbsCoin {
return sat / Math.pow(10, decimal);
}
}
public enum AddressType {
P2PKH,
P2SH,
SegWit
}
public static class Deriver extends AbsDeriver {
@Override
public String derive(String accountXpub, int changeIndex, int addressIndex) {
DeterministicKey address = getAddrDeterministicKey(accountXpub, changeIndex, addressIndex);
LegacyAddress addr = LegacyAddress.fromScriptHash(MAINNET,
segWitOutputScript(address.getPubKeyHash()).getPubKeyHash());
return addr.toBase58();
}
@Override
public String derive(String xPubKey) {
DeterministicKey key = DeterministicKey.deserializeB58(xPubKey, MAINNET);
return LegacyAddress.fromScriptHash(MAINNET,
segWitOutputScript(key.getPubKeyHash()).getPubKeyHash()).toBase58();
}
protected Script segWitOutputScript(byte[] pubKeyHash) {
return ScriptBuilder.createP2SHOutputScript(segWitRedeemScript(pubKeyHash));
}
private Script segWitRedeemScript(byte[] pubKeyHash) {
return new ScriptBuilder().smallNum(0).data(pubKeyHash).build();
}
}
}

72
coinlib/src/main/java/com/cobo/coinlib/coins/BTC/BtcImpl.java

@ -20,12 +20,24 @@ package com.cobo.coinlib.coins.BTC;
import androidx.annotation.NonNull;
import com.cobo.coinlib.coins.AbsTx;
import com.cobo.coinlib.coins.SignPsbtResult;
import com.cobo.coinlib.coins.SignTxResult;
import com.cobo.coinlib.interfaces.Signer;
import com.cobo.coinlib.v8.CoinImpl;
import com.eclipsesource.v8.V8Array;
import com.eclipsesource.v8.V8Function;
import com.eclipsesource.v8.V8Object;
import com.eclipsesource.v8.V8ScriptExecutionException;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.stream.Stream;
public class BtcImpl extends CoinImpl {
private V8Function parsePsbt;
private V8Function signPsbt;
public BtcImpl() {
super("BTC");
}
@ -34,4 +46,64 @@ public class BtcImpl extends CoinImpl {
V8Object txData = constructTxData(tx.getMetaData());
return signTxImpl(txData, "generateOmniTransactionSync", signers);
}
SignPsbtResult signPsbt(@NonNull String psbt, Signer... signers) {
if (this.signPsbt == null) {
this.signPsbt = (V8Function) coin.get("signPSBTBase64Sync");
}
v8.registerResource(signPsbt);
V8Array params = new V8Array(v8);
v8.registerResource(params);
params.push(psbt);
if (signers.length > 0) {
V8Array signProviders = new V8Array(v8);
v8.registerResource(signProviders);
Stream.of(signers).forEach(signer -> signProviders.push(createSignerProvider(signer)));
params.push(signProviders);
} else {
return null;
}
try {
V8Object object = (V8Object) signPsbt.call(coin, params);
return new SignPsbtResult(object.getString("txId"),
object.getString("psbtB64"));
} catch (V8ScriptExecutionException e) {
e.printStackTrace();
} finally {
v8.release(false);
}
return null;
}
JSONObject parsePsbt(@NonNull String psbtBase64) {
if (this.parsePsbt == null) {
this.parsePsbt = (V8Function) coin.get("parsePsbt");
}
v8.registerResource(parsePsbt);
V8Array params = new V8Array(v8);
v8.registerResource(params);
params.push(psbtBase64);
try {
V8Object result = (V8Object) parsePsbt.call(coin, params);
v8.registerResource(result);
V8Object json = v8.getObject("JSON");
v8.registerResource(json);
V8Array parameters = new V8Array(v8).push(result);
v8.registerResource(parameters);
String jsonResult = json.executeStringFunction("stringify", parameters);
return new JSONObject(jsonResult);
} catch (V8ScriptExecutionException | JSONException e) {
e.printStackTrace();
} finally {
v8.release(false);
}
return null;
}
}

68
coinlib/src/main/java/com/cobo/coinlib/coins/BTC/Deriver.java

@ -0,0 +1,68 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.coinlib.coins.BTC;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.SegwitAddress;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDKeyDerivation;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import static com.cobo.coinlib.coins.BTC.Btc.AddressType.P2PKH;
import static com.cobo.coinlib.coins.BTC.Btc.AddressType.P2SH;
import static com.cobo.coinlib.coins.BTC.Btc.AddressType.SegWit;
public class Deriver {
protected DeterministicKey getAddrDeterministicKey(String accountXpub, int changeIndex, int addressIndex) {
DeterministicKey account = DeterministicKey.deserializeB58(accountXpub, MainNetParams.get());
DeterministicKey change = HDKeyDerivation.deriveChildKey(account, changeIndex);
return HDKeyDerivation.deriveChildKey(change, addressIndex);
}
public String derive(String accountXpub,
int changeIndex,
int addressIndex,
Btc.AddressType type) {
DeterministicKey address = getAddrDeterministicKey(accountXpub, changeIndex, addressIndex);
if (type == P2PKH) {
return LegacyAddress.fromPubKeyHash(MainNetParams.get(),address.getPubKeyHash())
.toBase58();
} else if(type == SegWit) {
return SegwitAddress.fromHash(MainNetParams.get(), address.getPubKeyHash()).toBech32();
} else if (type == P2SH){
return LegacyAddress.fromScriptHash(MainNetParams.get(),
segWitOutputScript(address.getPubKeyHash()).getPubKeyHash()).toBase58();
} else {
throw new IllegalArgumentException();
}
}
protected Script segWitOutputScript(byte[] pubKeyHash) {
return ScriptBuilder.createP2SHOutputScript(segWitRedeemScript(pubKeyHash));
}
private Script segWitRedeemScript(byte[] pubKeyHash) {
return new ScriptBuilder().smallNum(0).data(pubKeyHash).build();
}
}

34
coinlib/src/main/java/com/cobo/coinlib/coins/SignPsbtResult.java

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 Cobo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* in the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
package com.cobo.coinlib.coins;
import android.text.TextUtils;
public class SignPsbtResult {
public final String txId;
public final String psbtB64;
public SignPsbtResult(String txId, String psbtB64) {
this.txId = txId;
this.psbtB64 = psbtB64;
}
public boolean isValid() {
return !TextUtils.isEmpty(txId) && !TextUtils.isEmpty(psbtB64);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save