mirror of
https://codeberg.org/anoncontributorxmr/mysu.git
synced 2024-12-22 13:07:46 +00:00
Basic onboarding flow, allows for wallet passwords.
NOTE: This commit still logs seeds to files for dev purposes, as there is no UI for it yet.
This commit is contained in:
parent
6674f894b7
commit
528bc7c7c5
16 changed files with 384 additions and 297 deletions
|
@ -1,20 +1,30 @@
|
|||
package com.m2049r.xmrwallet;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.m2049r.xmrwallet.fragment.dialog.PasswordBottomSheetDialog;
|
||||
import com.m2049r.xmrwallet.fragment.dialog.SendBottomSheetDialog;
|
||||
import com.m2049r.xmrwallet.livedata.SingleLiveEvent;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.AddressService;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.HistoryService;
|
||||
import com.m2049r.xmrwallet.service.MoneroHandlerThread;
|
||||
import com.m2049r.xmrwallet.service.PrefService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements MoneroHandlerThread.Listener {
|
||||
public class MainActivity extends AppCompatActivity implements MoneroHandlerThread.Listener, PasswordBottomSheetDialog.PasswordListener {
|
||||
public final SingleLiveEvent restartEvents = new SingleLiveEvent();
|
||||
private MoneroHandlerThread thread = null;
|
||||
private TxService txService = null;
|
||||
private BalanceService balanceService = null;
|
||||
|
@ -25,21 +35,39 @@ public class MainActivity extends AppCompatActivity implements MoneroHandlerThre
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
init();
|
||||
File walletFile = new File(getApplicationInfo().dataDir, Constants.WALLET_NAME);
|
||||
new PrefService(this);
|
||||
|
||||
if(walletFile.exists()) {
|
||||
boolean promptPassword = PrefService.getInstance().getBoolean(Constants.PREF_USES_PASSWORD, false);
|
||||
if(!promptPassword) {
|
||||
init(walletFile, "");
|
||||
} else {
|
||||
PasswordBottomSheetDialog passwordDialog = new PasswordBottomSheetDialog();
|
||||
passwordDialog.listener = this;
|
||||
passwordDialog.show(getSupportFragmentManager(), null);
|
||||
}
|
||||
} else {
|
||||
navigate(R.id.onboarding_fragment);
|
||||
}
|
||||
}
|
||||
|
||||
private void navigate(int destination) {
|
||||
FragmentActivity activity = this;
|
||||
FragmentManager fm = activity.getSupportFragmentManager();
|
||||
NavHostFragment navHostFragment =
|
||||
(NavHostFragment) fm.findFragmentById(R.id.nav_host_fragment);
|
||||
if (navHostFragment != null) {
|
||||
navHostFragment.getNavController().navigate(destination);
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroHandlerThread getThread() {
|
||||
return thread;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
File walletFile = new File(getApplicationInfo().dataDir, "xmr_wallet");
|
||||
Wallet wallet = null;
|
||||
if (walletFile.exists()) {
|
||||
wallet = WalletManager.getInstance().openWallet(walletFile.getAbsolutePath(), "");
|
||||
} else {
|
||||
wallet = WalletManager.getInstance().createWallet(walletFile, "", "English", 0);
|
||||
}
|
||||
public void init(File walletFile, String password) {
|
||||
Wallet wallet = WalletManager.getInstance().openWallet(walletFile.getAbsolutePath(), password);
|
||||
WalletManager.getInstance().setProxy("127.0.0.1:9050");
|
||||
thread = new MoneroHandlerThread("WalletService", wallet, this);
|
||||
this.txService = new TxService(this, thread);
|
||||
|
@ -55,4 +83,16 @@ public class MainActivity extends AppCompatActivity implements MoneroHandlerThre
|
|||
this.balanceService.refreshBalance();
|
||||
this.addressService.refreshAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasswordSuccess(String password) {
|
||||
File walletFile = new File(getApplicationInfo().dataDir, Constants.WALLET_NAME);
|
||||
init(walletFile, password);
|
||||
restartEvents.call();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasswordFail() {
|
||||
Toast.makeText(this, R.string.bad_password, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package com.m2049r.xmrwallet.fragment.dialog;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class PasswordBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
public PasswordListener listener = null;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.password_bottom_sheet_dialog, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
File walletFile = new File(getActivity().getApplicationInfo().dataDir, Constants.WALLET_NAME);
|
||||
|
||||
ImageButton pastePasswordImageButton = view.findViewById(R.id.paste_password_imagebutton);
|
||||
EditText passwordEditText = view.findViewById(R.id.wallet_password_edittext);
|
||||
Button unlockWalletButton = view.findViewById(R.id.unlock_wallet_button);
|
||||
|
||||
pastePasswordImageButton.setOnClickListener(view1 -> {
|
||||
passwordEditText.setText(Helper.getClipBoardText(view.getContext()));
|
||||
});
|
||||
|
||||
unlockWalletButton.setOnClickListener(view1 -> {
|
||||
String password = passwordEditText.getText().toString();
|
||||
boolean success = checkPassword(walletFile, password);
|
||||
if(success) {
|
||||
listener.onPasswordSuccess(password);
|
||||
dismiss();
|
||||
} else {
|
||||
listener.onPasswordFail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean checkPassword(File walletFile, String password) {
|
||||
Wallet wallet = WalletManager.getInstance().openWallet(walletFile.getAbsolutePath(), password);
|
||||
boolean ok = wallet.getStatus().isOk();
|
||||
wallet.close();
|
||||
return ok;
|
||||
}
|
||||
|
||||
public interface PasswordListener {
|
||||
void onPasswordSuccess(String password);
|
||||
void onPasswordFail();
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
package com.m2049r.xmrwallet.fragment.dialog;
|
||||
|
||||
import android.content.ClipboardManager;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -21,7 +21,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
private MutableLiveData<Boolean> _sendingMax = new MutableLiveData<>(false);
|
||||
|
@ -42,12 +41,8 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
|||
Button sendButton = view.findViewById(R.id.send_button);
|
||||
TextView sendAllTextView = view.findViewById(R.id.sending_all_textview);
|
||||
|
||||
TxService.getInstance().clearSendEvent.observe(getViewLifecycleOwner(), o -> {
|
||||
dismiss();
|
||||
});
|
||||
|
||||
pasteAddressImageButton.setOnClickListener(view1 -> {
|
||||
|
||||
addressEditText.setText(Helper.getClipBoardText(view.getContext()));
|
||||
});
|
||||
|
||||
sendMaxButton.setOnClickListener(view1 -> {
|
||||
|
@ -68,7 +63,12 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
|||
return;
|
||||
}
|
||||
sendButton.setEnabled(false);
|
||||
TxService.getInstance().sendTx(address, amount, sendAll);
|
||||
boolean success = TxService.getInstance().sendTx(address, amount, sendAll);
|
||||
if(success) {
|
||||
dismiss();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.error_sending_tx), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (!validAddress) {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
|
|
|
@ -16,6 +16,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
@ -32,7 +33,9 @@ import com.m2049r.xmrwallet.model.Wallet;
|
|||
import com.m2049r.xmrwallet.service.AddressService;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.HistoryService;
|
||||
import com.m2049r.xmrwallet.service.PrefService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
|
@ -49,9 +52,19 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
|
|||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
MainActivity mainActivity = (MainActivity)getActivity();
|
||||
mViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
|
||||
bindObservers(view);
|
||||
bindListeners(view);
|
||||
|
||||
boolean usesPassword = PrefService.getInstance().getBoolean(Constants.PREF_USES_PASSWORD, false);
|
||||
if(!usesPassword) {
|
||||
bindObservers(view);
|
||||
bindListeners(view);
|
||||
} else {
|
||||
mainActivity.restartEvents.observe(getViewLifecycleOwner(), o -> {
|
||||
bindObservers(view);
|
||||
bindListeners(view);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void bindListeners(View view) {
|
||||
|
@ -79,31 +92,38 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
|
|||
TextView unlockedBalanceTextView = view.findViewById(R.id.balance_unlocked_textview);
|
||||
TextView lockedBalanceTextView = view.findViewById(R.id.balance_locked_textview);
|
||||
|
||||
BalanceService.getInstance().balance.observe(getViewLifecycleOwner(), balance -> {
|
||||
unlockedBalanceTextView.setText(getString(R.string.wallet_balance_text, Wallet.getDisplayAmount(balance)));
|
||||
});
|
||||
BalanceService balanceService = BalanceService.getInstance();
|
||||
HistoryService historyService = HistoryService.getInstance();
|
||||
|
||||
BalanceService.getInstance().lockedBalance.observe(getViewLifecycleOwner(), lockedBalance -> {
|
||||
if(lockedBalance == 0) {
|
||||
lockedBalanceTextView.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
lockedBalanceTextView.setText(getString(R.string.wallet_locked_balance_text, Wallet.getDisplayAmount(lockedBalance)));
|
||||
lockedBalanceTextView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
if(balanceService != null) {
|
||||
balanceService.balance.observe(getViewLifecycleOwner(), balance -> {
|
||||
unlockedBalanceTextView.setText(getString(R.string.wallet_balance_text, Wallet.getDisplayAmount(balance)));
|
||||
});
|
||||
|
||||
balanceService.lockedBalance.observe(getViewLifecycleOwner(), lockedBalance -> {
|
||||
if (lockedBalance == 0) {
|
||||
lockedBalanceTextView.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
lockedBalanceTextView.setText(getString(R.string.wallet_locked_balance_text, Wallet.getDisplayAmount(lockedBalance)));
|
||||
lockedBalanceTextView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TransactionInfoAdapter adapter = new TransactionInfoAdapter(this);
|
||||
txHistoryRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
txHistoryRecyclerView.setAdapter(adapter);
|
||||
HistoryService.getInstance().history.observe(getViewLifecycleOwner(), history -> {
|
||||
if(history.isEmpty()) {
|
||||
txHistoryRecyclerView.setVisibility(View.GONE);
|
||||
} else {
|
||||
Collections.sort(history);
|
||||
adapter.submitList(history);
|
||||
txHistoryRecyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
if(historyService != null) {
|
||||
historyService.history.observe(getViewLifecycleOwner(), history -> {
|
||||
if (history.isEmpty()) {
|
||||
txHistoryRecyclerView.setVisibility(View.GONE);
|
||||
} else {
|
||||
Collections.sort(history);
|
||||
adapter.submitList(history);
|
||||
txHistoryRecyclerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package com.m2049r.xmrwallet.fragment.onboarding;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.PrefService;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class OnboardingFragment extends Fragment {
|
||||
|
||||
private OnboardingViewModel mViewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_settings, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mViewModel = new ViewModelProvider(this).get(OnboardingViewModel.class);
|
||||
EditText walletPasswordEditText = view.findViewById(R.id.wallet_password_edittext);
|
||||
EditText walletSeedEditText = view.findViewById(R.id.wallet_seed_edittext);
|
||||
Button createWalletButton = view.findViewById(R.id.create_wallet_button);
|
||||
createWalletButton.setOnClickListener(view1 -> {
|
||||
String walletPassword = walletPasswordEditText.getText().toString();
|
||||
if(!walletPassword.isEmpty()) {
|
||||
PrefService.getInstance().edit().putBoolean(Constants.PREF_USES_PASSWORD, true).apply();
|
||||
}
|
||||
String walletSeed = walletSeedEditText.getText().toString().trim();
|
||||
File walletFile = new File(getActivity().getApplicationInfo().dataDir, Constants.WALLET_NAME);
|
||||
Wallet wallet = null;
|
||||
if(walletSeed.isEmpty()) {
|
||||
wallet = WalletManager.getInstance().createWallet(walletFile, walletPassword, Constants.MNEMONIC_LANGUAGE, 0);
|
||||
} else {
|
||||
wallet = WalletManager.getInstance().recoveryWallet(walletFile, walletPassword, walletSeed, "", 0);
|
||||
}
|
||||
wallet.close();
|
||||
((MainActivity)getActivity()).init(walletFile, walletPassword);
|
||||
getActivity().onBackPressed();
|
||||
});
|
||||
walletSeedEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
String text = editable.toString();
|
||||
if(text.isEmpty()) {
|
||||
createWalletButton.setText(R.string.create_wallet);
|
||||
} else {
|
||||
createWalletButton.setText(R.string.menu_restore);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.m2049r.xmrwallet.fragment.onboarding;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
public class OnboardingViewModel extends ViewModel {
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.m2049r.xmrwallet.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
|
||||
public class PrefService extends ServiceBase {
|
||||
public static SharedPreferences instance = null;
|
||||
|
||||
public static SharedPreferences getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public PrefService(MainActivity mainActivity) {
|
||||
super(mainActivity, null);
|
||||
instance = mainActivity.getSharedPreferences(mainActivity.getApplicationInfo().packageName, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
|
@ -10,18 +10,12 @@ public class TxService extends ServiceBase {
|
|||
return instance;
|
||||
}
|
||||
|
||||
private final SingleLiveEvent _clearSendEvent = new SingleLiveEvent();
|
||||
public SingleLiveEvent clearSendEvent = _clearSendEvent;
|
||||
|
||||
public TxService(MainActivity mainActivity, MoneroHandlerThread thread) {
|
||||
super(mainActivity, thread);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public void sendTx(String address, String amount, boolean sendAll) {
|
||||
boolean success = this.getThread().sendTx(address, amount, sendAll);
|
||||
if (success) {
|
||||
_clearSendEvent.call();
|
||||
}
|
||||
public boolean sendTx(String address, String amount, boolean sendAll) {
|
||||
return this.getThread().sendTx(address, amount, sendAll);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package com.m2049r.xmrwallet.util;
|
||||
|
||||
public class Constants {
|
||||
public static final String WALLET_NAME = "xmr_wallet";
|
||||
public static final String MNEMONIC_LANGUAGE = "English";
|
||||
public static final String PREF_USES_PASSWORD = "pref_uses_password";
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020 m2049r et al.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.util;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
|
||||
public class FingerprintHelper {
|
||||
|
||||
public static boolean isDeviceSupported(Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FingerprintManager fingerprintManager = context.getSystemService(FingerprintManager.class);
|
||||
KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
|
||||
|
||||
return (keyguardManager != null) && (fingerprintManager != null) &&
|
||||
keyguardManager.isKeyguardSecure() &&
|
||||
fingerprintManager.isHardwareDetected() &&
|
||||
fingerprintManager.hasEnrolledFingerprints();
|
||||
}
|
||||
|
||||
public static boolean isFingerPassValid(Context context, String wallet) {
|
||||
try {
|
||||
KeyStoreHelper.loadWalletUserPass(context, wallet);
|
||||
return true;
|
||||
} catch (KeyStoreHelper.BrokenPasswordStoreException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void authenticate(Context context, CancellationSignal cancelSignal,
|
||||
FingerprintManager.AuthenticationCallback callback) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return;
|
||||
}
|
||||
|
||||
FingerprintManager manager = context.getSystemService(FingerprintManager.class);
|
||||
if (manager != null) {
|
||||
manager.authenticate(null, cancelSignal, 0, callback, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -295,22 +295,6 @@ public class Helper {
|
|||
return data;
|
||||
}
|
||||
|
||||
static public void setMoneroHome(Context context) {
|
||||
try {
|
||||
String home = getStorage(context, MONERO_DIR).getAbsolutePath();
|
||||
Os.setenv("HOME", home, true);
|
||||
} catch (ErrnoException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static public void initLogger(Context context) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
initLogger(context, WalletManager.LOGLEVEL_DEBUG);
|
||||
}
|
||||
// no logger if not debug
|
||||
}
|
||||
|
||||
// TODO make the log levels refer to the WalletManagerFactory::LogLevel enum ?
|
||||
static public void initLogger(Context context, int level) {
|
||||
String home = getStorage(context, MONERO_DIR).getAbsolutePath();
|
||||
|
@ -318,88 +302,4 @@ public class Helper {
|
|||
if (level >= WalletManager.LOGLEVEL_SILENT)
|
||||
WalletManager.setLogLevel(level);
|
||||
}
|
||||
|
||||
static public boolean useCrazyPass(Context context) {
|
||||
File flagFile = new File(getWalletRoot(context), NOCRAZYPASS_FLAGFILE);
|
||||
return !flagFile.exists();
|
||||
}
|
||||
|
||||
// try to figure out what the real wallet password is given the user password
|
||||
// which could be the actual wallet password or a (maybe malformed) CrAzYpass
|
||||
// or the password used to derive the CrAzYpass for the wallet
|
||||
static public String getWalletPassword(Context context, String walletName, String password) {
|
||||
String walletPath = new File(getWalletRoot(context), walletName + ".keys").getAbsolutePath();
|
||||
|
||||
// try with entered password (which could be a legacy password or a CrAzYpass)
|
||||
if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, password)) {
|
||||
return password;
|
||||
}
|
||||
|
||||
// maybe this is a malformed CrAzYpass?
|
||||
String possibleCrazyPass = CrazyPassEncoder.reformat(password);
|
||||
if (possibleCrazyPass != null) { // looks like a CrAzYpass
|
||||
if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, possibleCrazyPass)) {
|
||||
return possibleCrazyPass;
|
||||
}
|
||||
}
|
||||
|
||||
// generate & try with CrAzYpass
|
||||
String crazyPass = KeyStoreHelper.getCrazyPass(context, password);
|
||||
if (WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, crazyPass)) {
|
||||
return crazyPass;
|
||||
}
|
||||
|
||||
// or maybe it is a broken CrAzYpass? (of which we have two variants)
|
||||
String brokenCrazyPass2 = KeyStoreHelper.getBrokenCrazyPass(context, password, 2);
|
||||
if ((brokenCrazyPass2 != null)
|
||||
&& WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass2)) {
|
||||
return brokenCrazyPass2;
|
||||
}
|
||||
String brokenCrazyPass1 = KeyStoreHelper.getBrokenCrazyPass(context, password, 1);
|
||||
if ((brokenCrazyPass1 != null)
|
||||
&& WalletManager.getInstance().verifyWalletPasswordOnly(walletPath, brokenCrazyPass1)) {
|
||||
return brokenCrazyPass1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static AlertDialog openDialog = null; // for preventing opening of multiple dialogs
|
||||
static AsyncTask<Void, Void, Boolean> passwordTask = null;
|
||||
|
||||
public interface PasswordAction {
|
||||
void act(String walletName, String password, boolean fingerprintUsed);
|
||||
|
||||
void fail(String walletName);
|
||||
}
|
||||
|
||||
static private boolean processPasswordEntry(Context context, String walletName, String pass, boolean fingerprintUsed, PasswordAction action) {
|
||||
String walletPassword = Helper.getWalletPassword(context, walletName, pass);
|
||||
if (walletPassword != null) {
|
||||
action.act(walletName, walletPassword, fingerprintUsed);
|
||||
return true;
|
||||
} else {
|
||||
action.fail(walletName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Action {
|
||||
boolean run();
|
||||
}
|
||||
|
||||
static public boolean runWithNetwork(Action action) {
|
||||
StrictMode.ThreadPolicy currentPolicy = StrictMode.getThreadPolicy();
|
||||
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitNetwork().build();
|
||||
StrictMode.setThreadPolicy(policy);
|
||||
try {
|
||||
return action.run();
|
||||
} finally {
|
||||
StrictMode.setThreadPolicy(currentPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
static public boolean preventScreenshot() {
|
||||
return !(BuildConfig.DEBUG || BuildConfig.FLAVOR_type.equals("alpha"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,55 +62,6 @@ public class KeyStoreHelper {
|
|||
System.loadLibrary("monerujo");
|
||||
}
|
||||
|
||||
public static native byte[] slowHash(byte[] data, int brokenVariant);
|
||||
|
||||
static final private String RSA_ALIAS = "MonerujoRSA";
|
||||
|
||||
private static String getCrazyPass(Context context, String password, int brokenVariant) {
|
||||
byte[] data = password.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] sig = null;
|
||||
try {
|
||||
KeyStoreHelper.createKeys(context, RSA_ALIAS);
|
||||
sig = KeyStoreHelper.signData(RSA_ALIAS, data);
|
||||
byte[] hash = slowHash(sig, brokenVariant);
|
||||
if (hash == null) {
|
||||
throw new IllegalStateException("Slow Hash is null!");
|
||||
}
|
||||
return CrazyPassEncoder.encode(hash);
|
||||
} catch (NoSuchProviderException | NoSuchAlgorithmException |
|
||||
InvalidAlgorithmParameterException | KeyStoreException |
|
||||
InvalidKeyException | SignatureException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCrazyPass(Context context, String password) {
|
||||
if (Helper.useCrazyPass(context))
|
||||
return getCrazyPass(context, password, 0);
|
||||
else
|
||||
return password;
|
||||
}
|
||||
|
||||
public static String getBrokenCrazyPass(Context context, String password, int brokenVariant) {
|
||||
// due to a link bug in the initial implementation, some crazypasses were built with
|
||||
// prehash & variant == 1
|
||||
// since there are wallets out there, we need to keep this here
|
||||
// yes, it's a mess
|
||||
if (isArm32() && (brokenVariant != 2)) return null;
|
||||
return getCrazyPass(context, password, brokenVariant);
|
||||
}
|
||||
|
||||
private static Boolean isArm32 = null;
|
||||
|
||||
public static boolean isArm32() {
|
||||
if (isArm32 != null) return isArm32;
|
||||
synchronized (KeyStoreException.class) {
|
||||
if (isArm32 != null) return isArm32;
|
||||
isArm32 = Build.SUPPORTED_ABIS[0].equals("armeabi-v7a");
|
||||
return isArm32;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean saveWalletUserPass(@NonNull Context context, String wallet, String password) {
|
||||
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
|
||||
byte[] data = password.getBytes(StandardCharsets.UTF_8);
|
||||
|
@ -141,38 +92,6 @@ public class KeyStoreHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static boolean hasStoredPasswords(@NonNull Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
return prefs.getAll().size() > 0;
|
||||
}
|
||||
|
||||
public static String loadWalletUserPass(@NonNull Context context, String wallet) throws BrokenPasswordStoreException {
|
||||
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
|
||||
String encoded = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(wallet, "");
|
||||
if (encoded.isEmpty()) throw new BrokenPasswordStoreException();
|
||||
byte[] data = Base64.decode(encoded, Base64.DEFAULT);
|
||||
byte[] decrypted = KeyStoreHelper.decrypt(walletKeyAlias, data);
|
||||
if (decrypted == null) throw new BrokenPasswordStoreException();
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static void removeWalletUserPass(Context context, String wallet) {
|
||||
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
|
||||
try {
|
||||
KeyStoreHelper.deleteKeys(walletKeyAlias);
|
||||
} catch (KeyStoreException ex) {
|
||||
Timber.w(ex);
|
||||
}
|
||||
context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.remove(wallet).apply();
|
||||
}
|
||||
|
||||
public static void copyWalletUserPass(Context context, String srcWallet, String dstWallet) throws BrokenPasswordStoreException {
|
||||
final String pass = loadWalletUserPass(context, srcWallet);
|
||||
saveWalletUserPass(context, dstWallet, pass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a public and private key and stores it using the Android Key
|
||||
* Store, so that only this application will be able to access the keys.
|
||||
|
|
|
@ -6,14 +6,40 @@
|
|||
android:layout_height="match_parent"
|
||||
tools:context=".fragment.settings.SettingsFragment">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/monero_logo_imageview"
|
||||
android:layout_width="wrap_content"
|
||||
<EditText
|
||||
android:id="@+id/wallet_password_edittext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_monero_qr"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:hint="Password (optional)"
|
||||
android:inputType="textPassword"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_seed_edittext"
|
||||
tools:visibility="visible"/>
|
||||
<EditText
|
||||
android:id="@+id/wallet_seed_edittext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:hint="Recovery phrase (optional)"
|
||||
android:inputType="textPassword"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/create_wallet_button"
|
||||
tools:visibility="visible"/>
|
||||
<Button
|
||||
android:id="@+id/create_wallet_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/create_wallet"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_seed_edittext"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
49
app/src/main/res/layout/password_bottom_sheet_dialog.xml
Normal file
49
app/src/main/res/layout/password_bottom_sheet_dialog.xml
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:fitsSystemWindows="true"
|
||||
android:padding="16dp">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/wallet_password_edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:hint="Password"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/unlock_wallet_button"/>
|
||||
<ImageButton
|
||||
android:id="@+id/paste_password_imagebutton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_content_paste_24dp"
|
||||
app:layout_constraintTop_toTopOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintBottom_toBottomOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintEnd_toEndOf="@id/wallet_password_edittext"/>
|
||||
<Button
|
||||
android:id="@+id/unlock_wallet_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="Unlock"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
|
@ -16,6 +16,13 @@
|
|||
android:name="address"
|
||||
app:argType="string" />
|
||||
</action>
|
||||
<action
|
||||
android:id="@+id/nav_to_onboarding"
|
||||
app:destination="@id/onboarding_fragment">
|
||||
<argument
|
||||
android:name="address"
|
||||
app:argType="string" />
|
||||
</action>
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settings_fragment"
|
||||
|
@ -23,4 +30,10 @@
|
|||
android:label="fragment_send_amount"
|
||||
tools:layout="@layout/fragment_settings">
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/onboarding_fragment"
|
||||
android:name="com.m2049r.xmrwallet.fragment.onboarding.OnboardingFragment"
|
||||
android:label="fragment_onboarding"
|
||||
tools:layout="@layout/fragment_settings">
|
||||
</fragment>
|
||||
</navigation>
|
|
@ -541,4 +541,7 @@
|
|||
<string name="send_amount_invalid">Please enter a valid amount</string>
|
||||
<string name="send_max">Send Max</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="error_sending_tx">Error sending tx</string>
|
||||
<string name="create_wallet">Create wallet</string>
|
||||
<string name="invalid_mnemonic_code">Invalid mnemonic</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue