1
mirror of https://github.com/m2049r/xmrwallet synced 2024-11-23 05:33:29 +01:00

Use EXOLIX as exchange (#964)

This commit is contained in:
m2049r 2024-11-01 19:51:28 +01:00 committed by GitHub
parent 4ebcda2b14
commit 84ce392192
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
199 changed files with 3567 additions and 4115 deletions

View File

@ -8,8 +8,8 @@ android {
compileSdk 35
minSdkVersion 21
targetSdkVersion 35
versionCode 4008
versionName "4.0.8 'Sidekick'"
versionCode 4102
versionName "4.1.2 'Exolix'"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
@ -58,7 +58,7 @@ android {
applicationIdSuffix ".debug"
}
applicationVariants.all { variant ->
variant.buildConfigField "String", "ID_A", "\"" + getId("ID_A") + "\""
variant.buildConfigField "String", "ID_F", "\"" + getId("ID_F") + "\""
}
}

View File

@ -99,7 +99,7 @@ public class GenerateReviewFragment extends Fragment {
private String walletPath;
private String walletName;
private OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
// nothing
@ -164,6 +164,7 @@ public class GenerateReviewFragment extends Fragment {
});
Bundle args = getArguments();
assert args != null;
type = args.getString(REQUEST_TYPE);
walletPath = args.getString(REQUEST_PATH);
localPassword = args.getString(REQUEST_PASSWORD);

View File

@ -22,13 +22,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import timber.log.Timber;
public class LockFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Timber.d("onCreateView");
final FrameLayout frame = new FrameLayout(requireContext());
frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

View File

@ -619,6 +619,7 @@ public class LoginActivity extends BaseActivity
try {
GenerateReviewFragment detailsFragment = (GenerateReviewFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
assert detailsFragment != null;
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
if (dialog != null) {
Helper.showKeyboard(dialog);
@ -896,6 +897,7 @@ public class LoginActivity extends BaseActivity
try {
GenerateFragment genFragment = (GenerateFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
assert genFragment != null;
genFragment.walletGenerateError();
} catch (ClassCastException ex) {
Timber.e("walletGenerateError() but not in GenerateFragment");
@ -1366,7 +1368,7 @@ public class LoginActivity extends BaseActivity
private void registerDetachReceiver() {
detachReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
unregisterDetachReceiver();
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
Timber.i("Ledger detached!");
@ -1409,6 +1411,7 @@ public class LoginActivity extends BaseActivity
Timber.d("onDeviceConnected: %s", connectedDeviceName);
try {
SidekickConnectFragment f = (SidekickConnectFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container);
assert f != null;
f.allowClick();
} catch (ClassCastException ex) {
// ignore it

View File

@ -152,7 +152,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
showNetwork();
}
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
animateFAB();
@ -283,7 +283,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
// remove information of non-existent wallet
Set<String> removedWallets = getActivity()
Set<String> removedWallets = requireActivity()
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
.getAll().keySet();
for (WalletManager.WalletInfo s : walletList) {
@ -445,7 +445,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
private void setSubtext(String status) {
final Context ctx = getContext();
final Context ctx = requireContext();
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),

View File

@ -32,6 +32,7 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
@ -68,7 +69,7 @@ public class NodeFragment extends Fragment
static private int NODES_TO_FIND = 10;
static private NumberFormat FORMATTER = NumberFormat.getInstance();
static private final NumberFormat FORMATTER = NumberFormat.getInstance();
private SwipeRefreshLayout pullToRefresh;
private TextView tvPull;
@ -104,7 +105,7 @@ public class NodeFragment extends Fragment
}
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Listener) {
this.activityCallback = (Listener) context;
@ -146,7 +147,7 @@ public class NodeFragment extends Fragment
}
}
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
Toast.makeText(requireActivity(), getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show();
@ -210,7 +211,7 @@ public class NodeFragment extends Fragment
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.node_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@ -454,7 +455,7 @@ public class NodeFragment extends Fragment
private void closeDialog() {
if (editDialog == null) throw new IllegalStateException();
Helper.hideKeyboardAlways(getActivity());
Helper.hideKeyboardAlways(requireActivity());
editDialog.dismiss();
editDialog = null;
NodeFragment.this.editDialog = null;

View File

@ -32,12 +32,10 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -84,8 +82,6 @@ public class ReceiveFragment extends Fragment {
private ImageView ivQrCode;
private ImageView ivQrCodeFull;
private EditText etDummy;
private ImageButton bCopyAddress;
private MenuItem shareItem;
private Wallet wallet = null;
private boolean isMyWallet = false;
@ -116,11 +112,10 @@ public class ReceiveFragment extends Fragment {
tvQrCode = view.findViewById(R.id.tvQrCode);
ivQrCodeFull = view.findViewById(R.id.qrCodeFull);
etDummy = view.findViewById(R.id.etDummy);
bCopyAddress = view.findViewById(R.id.bCopyAddress);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
bCopyAddress.setOnClickListener(v -> copyAddress());
view.findViewById(R.id.bCopyAddress).setOnClickListener(v -> copyAddress());
evAmount.setOnNewAmountListener(xmr -> {
Timber.d("new amount = %s", xmr);
@ -211,8 +206,7 @@ public class ReceiveFragment extends Fragment {
inflater.inflate(R.menu.receive_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
shareItem = menu.findItem(R.id.menu_item_share);
shareItem.setOnMenuItemClickListener(item -> {
menu.findItem(R.id.menu_item_share).setOnMenuItemClickListener(item -> {
if (shareRequested) return true;
shareRequested = true;
if (!qrValid) {
@ -238,7 +232,7 @@ public class ReceiveFragment extends Fragment {
private boolean saveQrCode() {
if (!qrValid) throw new IllegalStateException("trying to save null qr code!");
File cachePath = new File(getActivity().getCacheDir(), "images");
File cachePath = new File(requireActivity().getCacheDir(), "images");
if (!cachePath.exists())
if (!cachePath.mkdirs()) throw new IllegalStateException("cannot create images folder");
File png = new File(cachePath, "QR.png");
@ -452,7 +446,7 @@ public class ReceiveFragment extends Fragment {
.withEndAction(resetSize).start();
}
subaddress = newSubaddress;
final Context context = getContext();
final Context context = requireContext();
Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress,
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),

View File

@ -24,6 +24,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.google.zxing.BarcodeFormat;
@ -43,7 +44,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
private ZXingScannerView mScannerView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Timber.d("onCreateView");
mScannerView = new ZXingScannerView(getActivity());
return mScannerView;
@ -85,7 +86,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
}
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnScannedListener) {
this.onScannedListener = (OnScannedListener) context;

View File

@ -5,6 +5,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
@ -60,7 +61,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
private SettingsFragment.Listener activity;
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof SettingsFragment.Listener) {
activity = (SettingsFragment.Listener) context;

View File

@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@ -29,6 +30,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -73,6 +75,8 @@ public class SidekickConnectFragment extends Fragment
@Override
public void onPause() {
Timber.d("onPause()");
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
throw new IllegalStateException("Bluetooth permission not granted");
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
@ -112,6 +116,8 @@ public class SidekickConnectFragment extends Fragment
private void populateList() {
List<BluetoothInfo> items = new ArrayList<>();
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
throw new IllegalStateException("Bluetooth permission not granted");
for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
final int deviceCLass = device.getBluetoothClass().getDeviceClass();
switch (deviceCLass) {
@ -152,6 +158,8 @@ public class SidekickConnectFragment extends Fragment
// Make sure we're not doing discovery anymore
if (bluetoothAdapter != null) {
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
throw new IllegalStateException("Bluetooth permission not granted");
bluetoothAdapter.cancelDiscovery();
}
}
@ -159,6 +167,8 @@ public class SidekickConnectFragment extends Fragment
@Override
public void onInteraction(final View view, final BluetoothInfo item) {
Timber.d("onInteraction %s", item);
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
throw new IllegalStateException("Bluetooth permission not granted");
bluetoothAdapter.cancelDiscovery();
final BluetoothFragment btFragment = (BluetoothFragment) getChildFragmentManager().findFragmentById(R.id.bt_fragment);

View File

@ -226,7 +226,7 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
// Callbacks from SubaddressInfoAdapter
@Override
public void onInteraction(final View view, final Subaddress subaddress) {
public void onInteraction(final View view, @NonNull final Subaddress subaddress) {
if (managerMode)
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
else

View File

@ -52,7 +52,6 @@ public class SubaddressInfoFragment extends Fragment
private Subaddress subaddress;
private TextInputLayout etName;
private TextView tvAddress;
private TextView tvTxLabel;
@Override
@ -61,7 +60,6 @@ public class SubaddressInfoFragment extends Fragment
View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false);
etName = view.findViewById(R.id.etName);
tvAddress = view.findViewById(R.id.tvAddress);
tvTxLabel = view.findViewById(R.id.tvTxLabel);
final RecyclerView list = view.findViewById(R.id.list);
@ -71,11 +69,13 @@ public class SubaddressInfoFragment extends Fragment
final Wallet wallet = activityCallback.getWallet();
Bundle b = getArguments();
assert b != null;
final int subaddressIndex = b.getInt("subaddressIndex");
subaddress = wallet.getSubaddressObject(subaddressIndex);
etName.getEditText().setText(subaddress.getDisplayLabel());
tvAddress.setText(getContext().getString(R.string.subbaddress_info_subtitle,
final TextView tvAddress = view.findViewById(R.id.tvAddress);
tvAddress.setText(requireContext().getString(R.string.subbaddress_info_subtitle,
subaddress.getAddressIndex(), subaddress.getAddress()));
etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {

View File

@ -20,7 +20,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.InputType;
@ -34,6 +33,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.transition.Transition;
@ -44,6 +44,8 @@ import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Transfer;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import com.m2049r.xmrwallet.widget.Toolbar;
@ -61,6 +63,7 @@ public class TxFragment extends Fragment {
static public final String ARG_INFO = "info";
@SuppressLint("SimpleDateFormat")
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
public TxFragment() {
@ -106,6 +109,7 @@ public class TxFragment extends Fragment {
tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc);
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
tvXmrToKeyLabel = view.findViewById(R.id.tvXmrToKeyLabel);
tvXmrToLogo = view.findViewById(R.id.tvXmrToLogo);
tvAccount = view.findViewById(R.id.tvAccount);
@ -127,18 +131,20 @@ public class TxFragment extends Fragment {
tvWarning = view.findViewById(R.id.tvWarning);
tvTxXmrToKey.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
});
info = getArguments().getParcelable(ARG_INFO);
final Bundle args = getArguments();
assert args != null;
info = args.getParcelable(ARG_INFO);
show();
return view;
}
void shareTxInfo() {
if (this.info == null) return;
StringBuffer sb = new StringBuffer();
StringBuilder sb = new StringBuilder();
sb.append(getString(R.string.tx_timestamp)).append(":\n");
sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n\n");
@ -216,7 +222,7 @@ public class TxFragment extends Fragment {
private void showSubaddressLabel() {
final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex);
final Context ctx = getContext();
final Context ctx = requireContext();
Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted,
info.accountIndex, info.addressIndex,
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
@ -264,16 +270,17 @@ public class TxFragment extends Fragment {
tvTxFee.setVisibility(View.GONE);
}
final Context ctx = requireContext();
if (info.isFailed) {
tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount)));
tvTxFee.setText(getString(R.string.tx_list_failed_text));
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.neutralColor));
} else if (info.isPending) {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.neutralColor));
} else if (info.direction == TransactionInfo.Direction.Direction_In) {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor));
} else {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.negativeColor));
}
Set<String> destinations = new HashSet<>();
StringBuilder sb = new StringBuilder();
@ -328,31 +335,29 @@ public class TxFragment extends Fragment {
void showBtcInfo() {
if (userNotes.xmrtoKey != null) {
cvXmrTo.setVisibility(View.VISIBLE);
String key = userNotes.xmrtoKey;
if ("xmrto".equals(userNotes.xmrtoTag)) { // legacy xmr.to service :(
key = "xmrto-" + key;
}
tvTxXmrToKey.setText(key);
ShiftService service = ShiftService.findWithTag(userNotes.xmrtoTag);
tvXmrToKeyLabel.setText(getString(R.string.label_send_btc_xmrto_key_lb, service.getLabel()));
if (service.getIconId() == 0)
tvXmrToLogo.setVisibility(View.GONE);
else
tvXmrToLogo.setImageResource(service.getLogoId());
tvTxXmrToKey.setText(userNotes.xmrtoKey);
tvDestinationBtc.setText(userNotes.xmrtoDestination);
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " " + userNotes.xmrtoCurrency);
switch (userNotes.xmrtoTag) {
case "xmrto":
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
tvXmrToLogo.setImageResource(R.drawable.ic_xmrto_logo);
break;
case "side": // defaults in layout - just add underline
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvXmrToSupport.setOnClickListener(v -> {
Uri uri = Uri.parse("https://sideshift.ai/orders/" + userNotes.xmrtoKey);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
});
break;
default:
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
tvXmrToLogo.setVisibility(View.GONE);
ShiftApi shiftApi = service.getShiftApi();
if (shiftApi != null) {
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, service.getLabel()));
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvXmrToSupport.setOnClickListener(v -> {
startActivity(new Intent(Intent.ACTION_VIEW, shiftApi.getQueryOrderUri(userNotes.xmrtoKey)));
});
} else {
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
}
} else {
cvXmrTo.setVisibility(View.GONE);
@ -369,7 +374,7 @@ public class TxFragment extends Fragment {
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.tx_info_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@ -397,7 +402,7 @@ public class TxFragment extends Fragment {
}
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof TxFragment.Listener) {
this.activityCallback = (TxFragment.Listener) context;

View File

@ -38,7 +38,6 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.GravityCompat;
@ -348,7 +347,10 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
}
Bundle extras = getIntent().getExtras();
if (extras == null) finish(); // we need extras!
if (extras == null) {
finish(); // we need extras!
return;
}
String walletId = extras.getString(REQUEST_ID);
requestStreetMode = extras.getBoolean(REQUEST_STREETMODE);
@ -1179,7 +1181,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
}
@Override
public void onSubaddressSelected(@Nullable final Subaddress subaddress) {
public void onSubaddressSelected(@NonNull final Subaddress subaddress) {
selectedSubaddressIndex = subaddress.getAddressIndex();
getOnBackPressedDispatcher().onBackPressed();
}

View File

@ -112,7 +112,7 @@ public class WalletFragment extends Fragment
flExchange = view.findViewById(R.id.flExchange);
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
setColorFilter(
ThemeHelper.getThemedColor(getContext(), com.google.android.material.R.attr.colorPrimaryVariant),
ThemeHelper.getThemedColor(requireContext(), com.google.android.material.R.attr.colorPrimaryVariant),
android.graphics.PorterDuff.Mode.MULTIPLY);
tvProgress = view.findViewById(R.id.tvProgress);

View File

@ -23,7 +23,6 @@ import android.os.Build;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.util.LocaleHelper;
import com.m2049r.xmrwallet.util.NetCipherHelper;

View File

@ -24,43 +24,38 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
import lombok.ToString;
import timber.log.Timber;
@Getter
@ToString
public class BarcodeData {
public enum Security {
NORMAL,
OA_NO_DNSSEC,
OA_DNSSEC
}
final public Crypto asset;
final public List<Crypto> ambiguousAssets;
final public String address;
final public String addressName;
final public String amount;
final public String description;
final public Security security;
final private Set<Crypto> possibleAssets = new HashSet<>();
private String address = null;
private String addressName = null;
private String amount = null;
private String description = null;
private Security security = null;
public BarcodeData(List<Crypto> assets, String address) {
if (assets.isEmpty())
throw new IllegalArgumentException("no assets specified");
this.addressName = null;
this.description = null;
this.amount = null;
this.security = Security.NORMAL;
security = Security.NORMAL;
this.address = address;
if (assets.size() == 1) {
this.asset = assets.get(0);
this.ambiguousAssets = null;
} else {
this.asset = null;
this.ambiguousAssets = assets;
}
possibleAssets.addAll(assets);
}
public BarcodeData(Crypto asset, String address, String description, String amount) {
@ -68,8 +63,7 @@ public class BarcodeData {
}
public BarcodeData(Crypto asset, String address, String addressName, String description, String amount, Security security) {
this.ambiguousAssets = null;
this.asset = asset;
possibleAssets.add(asset);
this.address = address;
this.addressName = addressName;
this.description = description;
@ -82,7 +76,7 @@ public class BarcodeData {
}
public String getUriString() {
if (asset != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
if (getAsset() != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
StringBuilder sb = new StringBuilder();
sb.append(Crypto.XMR.getUriScheme())
.append(':')
@ -227,6 +221,20 @@ public class BarcodeData {
}
public boolean isAmbiguous() {
return ambiguousAssets != null;
return possibleAssets.size() > 1;
}
public Crypto getAsset() {
if (possibleAssets.size() == 1) {
return possibleAssets.iterator().next();
} else {
return null;
}
}
// return true if we still have possible assets
public boolean filter(Set<Crypto> assets) {
possibleAssets.retainAll(assets);
return !possibleAssets.isEmpty();
}
}

View File

@ -1,3 +1,19 @@
/*
* Copyright (c) 2024 m2049r
*
* 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.data;
import androidx.annotation.NonNull;
@ -5,35 +21,30 @@ import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
import java.util.regex.Pattern;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Crypto {
XMR("XMR", true, "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Wallet::isAddressValid),
BTC("BTC", true, "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC);
}),
DASH("DASH", true, "dash:amount:label:message", R.id.ibDASH, R.drawable.ic_xmrto_dash, R.drawable.ic_xmrto_dash_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
}),
DOGE("DOGE", true, "dogecoin:amount:label:message", R.id.ibDOGE, R.drawable.ic_xmrto_doge, R.drawable.ic_xmrto_doge_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DOGE);
}),
ETH("ETH", false, "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, EthAddressValidator::validate),
LTC("LTC", true, "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC);
});
XMR("XMR", "XMR", "XMR", "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Pattern.compile("^[48][a-zA-Z|\\d]{94}([a-zA-Z|\\d]{11})?$")),
BTC("BTC", "BTC", "BTC", "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, Pattern.compile("^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$|^(bc1q)|(bc1p)[0-9A-Za-z]{37,62}$")),
LTC("LTC", "LTC", "LTC", "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, Pattern.compile("^([LM3])[A-Za-z0-9]{33}$|^(ltc1)[0-9A-Za-z]{39}$")),
ETH("ETH", "ETH", "ETH", "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, Pattern.compile("^(0x)[0-9A-Fa-f]{40}$")),
USDT("USDT", "TRX", "USDT(TRC20)", "usdt:amount:label:message", R.id.ibUSDT, R.drawable.ic_xmrto_usdt_trc20, R.drawable.ic_xmrto_usdt_trc20_off, Pattern.compile("^T[1-9A-HJ-NP-Za-km-z]{33}$")),
SOLANA("SOL", "SOL", "SOL", "solana:amount:label:message", R.id.ibSOL, R.drawable.ic_xmrto_sol, R.drawable.ic_xmrto_sol_off, Pattern.compile("^[1-9A-HJ-NP-Za-km-z]{32,44}$"));
@Getter
@NonNull
private final String symbol;
@Getter
private final boolean casefull;
@NonNull
private final String network;
@Getter
@NonNull
private final String label;
@NonNull
private final String uriSpec;
@Getter
@ -43,7 +54,7 @@ public enum Crypto {
@Getter
private final int iconDisabledId;
@NonNull
private final Validator validator;
private final Pattern regex;
@Nullable
public static Crypto withScheme(@NonNull String scheme) {
@ -62,10 +73,6 @@ public enum Crypto {
return null;
}
interface Validator {
boolean validate(String address);
}
// TODO maybe cache these segments
String getUriScheme() {
return uriSpec.split(":")[0];
@ -83,7 +90,8 @@ public enum Crypto {
return uriSpec.split(":")[3];
}
boolean validate(String address) {
return validator.validate(address);
public boolean validate(String address) {
if (this == XMR) return Wallet.isAddressValid(address);
return regex.matcher(address).find();
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 m2049r
*
* 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.data;
import lombok.Value;
@Value
public class CryptoAmount {
Crypto crypto;
double amount;
public CryptoAmount newWithAmount(double amount) {
return new CryptoAmount(this.crypto, amount);
}
}

View File

@ -201,8 +201,13 @@ public class NodeInfo extends Node {
.port(port)
.addPathSegment("json_rpc")
.build();
final String json = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}";
return new Request(url, json, getUsername(), getPassword());
try {
final JSONObject json = new JSONObject("{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}");
return new Request(url, json, getUsername(), getPassword());
} catch (JSONException ex) {
throw new IllegalStateException(ex);
}
}
private boolean testRpcService(int port) {

View File

@ -20,22 +20,21 @@ import android.os.Parcel;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class TxDataBtc extends TxData {
@Getter
@Setter
private ShiftService shiftService;
private String btcSymbol; // the actual non-XMR thing we're sending
@Getter
@Setter
private String xmrtoOrderId; // shown in success screen
@Getter
@Setter
private String btcAddress;
@Getter
@Setter
private double btcAmount;
private CryptoAmount shiftAmount; // what we want to send
private String xmrtoQueryOrderToken; // used for queryOrder API
public TxDataBtc() {
super();
@ -47,7 +46,9 @@ public class TxDataBtc extends TxData {
out.writeString(btcSymbol);
out.writeString(xmrtoOrderId);
out.writeString(btcAddress);
out.writeDouble(btcAmount);
out.writeString(shiftAmount.getCrypto().name());
out.writeDouble(shiftAmount.getAmount());
out.writeString(xmrtoQueryOrderToken);
}
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
@ -66,7 +67,8 @@ public class TxDataBtc extends TxData {
btcSymbol = in.readString();
xmrtoOrderId = in.readString();
btcAddress = in.readString();
btcAmount = in.readDouble();
shiftAmount = new CryptoAmount(Crypto.valueOf(in.readString()), in.readDouble());
xmrtoQueryOrderToken = in.readString();
}
@NonNull
@ -79,19 +81,33 @@ public class TxDataBtc extends TxData {
sb.append(btcSymbol);
sb.append(",btcAddress:");
sb.append(btcAddress);
sb.append(",btcAmount:");
sb.append(btcAmount);
sb.append(",amount:");
sb.append(shiftAmount);
sb.append(",xmrtoQueryOrderToken:");
sb.append(xmrtoQueryOrderToken);
return sb.toString();
}
public boolean validateAddress(@NonNull String address) {
if ((btcSymbol == null) || (btcAddress == null)) return false;
final Crypto crypto = Crypto.withSymbol(btcSymbol);
if (crypto == null) return false;
if (crypto.isCasefull()) { // compare as-is
return address.equals(btcAddress);
} else { // normalize & compare (e.g. ETH with and without checksum capitals
return address.toLowerCase().equals(btcAddress.toLowerCase());
return address.equalsIgnoreCase(btcAddress);
}
public double getBtcAmount() {
return (shiftAmount.getCrypto() == Crypto.XMR) ? 0 : shiftAmount.getAmount();
}
public double getXmrAmount() {
return (shiftAmount.getCrypto() == Crypto.XMR) ? shiftAmount.getAmount() : 0;
}
public boolean validate(RequestQuote quote) {
if (shiftAmount.getCrypto() == Crypto.XMR) {
return (quote.getXmrAmount() == getXmrAmount());
} else {
return (quote.getBtcAmount() == getBtcAmount());
}
}
}

View File

@ -16,7 +16,7 @@
package com.m2049r.xmrwallet.data;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.util.Helper;
import java.util.regex.Matcher;
@ -61,7 +61,7 @@ public class UserNotes {
public void setXmrtoOrder(CreateOrder order) {
if (order != null) {
xmrtoTag = order.TAG;
xmrtoTag = order.getTag();
xmrtoKey = order.getOrderId();
xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount());
xmrtoCurrency = order.getBtcCurrency();

View File

@ -20,10 +20,10 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -57,13 +57,14 @@ public class AboutFragment extends DialogFragment {
AboutFragment.newInstance().show(ft, TAG);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_about, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_about, null);
((TextView) view.findViewById(R.id.tvHelp)).setText(Html.fromHtml(getLicencesHtml()));
((TextView) view.findViewById(R.id.tvVersion)).setText(getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {
@ -77,7 +78,7 @@ public class AboutFragment extends DialogFragment {
private String getLicencesHtml() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(getContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
new InputStreamReader(requireContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null)

View File

@ -20,10 +20,10 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -49,13 +49,14 @@ public class CreditsFragment extends DialogFragment {
CreditsFragment.newInstance().show(ft, TAG);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_credits, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_credits, null);
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.credits_text)));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {

View File

@ -16,13 +16,13 @@
package com.m2049r.xmrwallet.dialog;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
@ -65,9 +65,10 @@ public class HelpFragment extends DialogFragment {
private Spanned getHtml(String html, double textSize) {
final Html.ImageGetter imageGetter = source -> {
final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
@SuppressLint("DiscouragedApi") final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
// Don't die if we don't find the image - use a heart instead
final Drawable drawable = ContextCompat.getDrawable(requireActivity(), imageId > 0 ? imageId : R.drawable.ic_favorite_24dp);
assert drawable != null;
final double f = textSize / drawable.getIntrinsicHeight();
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
return drawable;
@ -82,7 +83,7 @@ public class HelpFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_help, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_help, null);
int helpId = 0;
boolean torButton = false;
@ -100,7 +101,7 @@ public class HelpFragment extends DialogFragment {
.setView(view);
if (torButton) {
builder.setNegativeButton(R.string.help_nok,
(dialog, id) -> dialog.dismiss())
(dialog, id) -> dialog.dismiss())
.setPositiveButton(R.string.help_getorbot,
(dialog, id) -> {
dialog.dismiss();

View File

@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
@ -66,7 +65,7 @@ public class PocketChangeFragment extends DialogFragment implements Slider.OnCha
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pocketchange_setting, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_pocketchange_setting, null);
boolean enabled = false;
int progress = 0;
Bundle arguments = getArguments();

View File

@ -20,10 +20,10 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -49,13 +49,14 @@ public class PrivacyFragment extends DialogFragment {
PrivacyFragment.newInstance().show(ft, TAG);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_privacy_policy, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_privacy_policy, null);
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.privacy_policy)));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {

View File

@ -17,6 +17,7 @@ package com.m2049r.xmrwallet.dialog;
* limitations under the License.
*/
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -31,6 +32,7 @@ import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.Helper;
import java.util.Locale;
import java.util.Objects;
import timber.log.Timber;
@ -56,7 +58,7 @@ public class ProgressDialog extends AlertDialog {
@Override
protected void onCreate(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null);
@SuppressLint("InflateParams") final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null);
pbCircle = view.findViewById(R.id.pbCircle);
tvMessage = view.findViewById(R.id.tvMessage);
rlProgressBar = view.findViewById(R.id.rlProgressBar);
@ -78,7 +80,7 @@ public class ProgressDialog extends AlertDialog {
super.onCreate(savedInstanceState);
if (Helper.preventScreenshot()) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
Objects.requireNonNull(getWindow()).setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
}

View File

@ -0,0 +1,16 @@
package com.m2049r.xmrwallet.fragment.send;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
public interface PreShifter {
CryptoAmount getAmount();
void onOrderParametersError(final Exception ex);
void onOrderParametersReceived(final QueryOrderParameters orderParameters);
boolean isActive();
void showProgress();
}

View File

@ -44,12 +44,9 @@ import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OpenAliasHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
import java.util.HashMap;
import java.util.HashSet;
@ -64,15 +61,11 @@ public class SendAddressWizardFragment extends SendWizardFragment {
public static SendAddressWizardFragment newInstance(Listener listener) {
SendAddressWizardFragment instance = new SendAddressWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
public void setSendListener(Listener listener) {
this.sendListener = listener;
}
private Listener sendListener;
public interface Listener {
void setBarcodeData(BarcodeData data);
@ -103,13 +96,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
void onScan();
}
private Crypto getCryptoForButton(ImageButton button) {
for (Map.Entry<Crypto, ImageButton> entry : ibCrypto.entrySet()) {
if (entry.getValue() == button) return entry.getKey();
}
return null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -122,7 +108,9 @@ public class SendAddressWizardFragment extends SendWizardFragment {
ibCrypto = new HashMap<>();
for (Crypto crypto : Crypto.values()) {
final ImageButton button = view.findViewById(crypto.getButtonId());
if (Helper.ALLOW_SHIFT || (crypto == Crypto.XMR)) {
if ((crypto == Crypto.XMR)
|| (Helper.ALLOW_SHIFT && ShiftService.isAssetSupported(crypto))) {
button.setVisibility(View.VISIBLE);
ibCrypto.put(crypto, button);
button.setOnClickListener(v -> {
if (possibleCryptos.contains(crypto)) {
@ -131,9 +119,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
} else {
// show help what to do:
if (button.getId() != R.id.ibXMR) {
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
final String symbol = getCryptoForButton(button).getSymbol();
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, crypto.getNetwork(), crypto.getLabel(), ShiftService.DEFAULT.getLabel())));
tvXmrTo.setVisibility(View.VISIBLE);
} else {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
@ -143,9 +129,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
});
} else {
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(128);
button.setEnabled(false);
button.setVisibility(View.INVISIBLE);
}
}
if (!Helper.ALLOW_SHIFT) {
@ -175,49 +159,26 @@ public class SendAddressWizardFragment extends SendWizardFragment {
@Override
public void afterTextChanged(Editable editable) {
Timber.d("AFTER: %s", editable.toString());
etAddress.setError(null);
BarcodeData bc = sendListener.getBarcodeData();
if (bc == null) {
final String address = etAddress.getEditText().getText().toString();
bc = BarcodeData.fromString(address);
}
sendListener.setBarcodeData(null); // it's used up now
possibleCryptos.clear();
selectedCrypto = null;
final String address = etAddress.getEditText().getText().toString();
if (isIntegratedAddress(address)) {
Timber.d("isIntegratedAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
etAddress.setError(getString(R.string.info_paymentid_integrated));
sendListener.setMode(SendFragment.Mode.XMR);
} else if (isStandardAddress(address)) {
Timber.d("isStandardAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
sendListener.setMode(SendFragment.Mode.XMR);
}
if (!Helper.ALLOW_SHIFT) return;
if ((selectedCrypto == null) && isEthAddress(address)) {
Timber.d("isEthAddress");
possibleCryptos.add(Crypto.ETH);
selectedCrypto = Crypto.ETH;
tvXmrTo.setVisibility(View.VISIBLE);
sendListener.setMode(SendFragment.Mode.BTC);
}
if (possibleCryptos.isEmpty()) {
Timber.d("isBitcoinAddress");
for (BitcoinAddressType type : BitcoinAddressType.values()) {
if (BitcoinAddressValidator.validate(address, type)) {
possibleCryptos.add(Crypto.valueOf(type.name()));
}
}
if (!possibleCryptos.isEmpty()) // found something in need of shifting!
sendListener.setMode(SendFragment.Mode.BTC);
if (possibleCryptos.size() == 1) {
selectedCrypto = (Crypto) possibleCryptos.toArray()[0];
if ((bc != null) && (bc.filter(ShiftService.getPossibleAssets()))) {
possibleCryptos.clear();
possibleCryptos.addAll(bc.getPossibleAssets());
selectedCrypto = bc.getAsset();
if (checkAddress()) {
if (bc.getSecurity() == BarcodeData.Security.OA_NO_DNSSEC)
etAddress.setError(getString(R.string.send_address_no_dnssec));
else if (bc.getSecurity() == BarcodeData.Security.OA_DNSSEC)
etAddress.setError(getString(R.string.send_address_openalias));
}
}
if (possibleCryptos.isEmpty()) {
Timber.d("other");
tvXmrTo.setVisibility(View.INVISIBLE);
sendListener.setMode(SendFragment.Mode.XMR);
}
updateCryptoButtons(address.isEmpty());
updateCryptoButtons(false);
}
@Override
@ -231,7 +192,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
bPasteAddress.setOnClickListener(v -> {
final String clip = Helper.getClipBoardText(getActivity());
final String clip = Helper.getClipBoardText(requireActivity());
if (clip == null) return;
// clean it up
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
@ -248,16 +209,14 @@ public class SendAddressWizardFragment extends SendWizardFragment {
etNotes = view.findViewById(R.id.etNotes);
etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT);
etNotes.getEditText().
setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
etDummy.requestFocus();
return true;
}
return false;
});
etNotes.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
etDummy.requestFocus();
return true;
}
return false;
});
final View cvScan = view.findViewById(R.id.bScan);
cvScan.setOnClickListener(v -> onScanListener.onScan());
@ -271,13 +230,19 @@ public class SendAddressWizardFragment extends SendWizardFragment {
private void selectedCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
assert button != null;
button.setImageResource(crypto.getIconEnabledId());
button.setImageAlpha(255);
button.setEnabled(true);
if (selectedCrypto == Crypto.XMR)
sendListener.setMode(SendFragment.Mode.XMR);
else
sendListener.setMode(SendFragment.Mode.BTC);
}
private void possibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
assert button != null;
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(255);
button.setEnabled(true);
@ -285,6 +250,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
private void impossibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
if (button == null) return; // not all buttons exist for all providers
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(128);
button.setEnabled(true);
@ -302,7 +268,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
}
if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getSymbol())));
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getNetwork(), selectedCrypto.getLabel(), ShiftService.DEFAULT.getLabel())));
tvXmrTo.setVisibility(View.VISIBLE);
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous)));
@ -328,7 +294,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
BarcodeData barcodeData = dataMap.get(Crypto.XMR);
if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC);
if (barcodeData != null) {
Timber.d("Security=%s, %s", barcodeData.security.toString(), barcodeData.address);
Timber.d("Security=%s, %s", barcodeData.getSecurity().toString(), barcodeData.getAddress());
processScannedData(barcodeData);
} else {
etAddress.setError(getString(R.string.send_address_not_openalias));
@ -369,18 +335,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
&& Wallet.isAddressValid(address);
}
private boolean isBitcoinishAddress(String address) {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
}
private boolean isEthAddress(String address) {
return EthAddressValidator.validate(address);
}
private void shakeAddress() {
if (possibleCryptos.size() > 1) { // address ambiguous
for (Crypto crypto : Crypto.values()) {
@ -412,10 +366,10 @@ public class SendAddressWizardFragment extends SendWizardFragment {
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
txData.setDestination(null);
ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase();
ShiftService.ASSET = selectedCrypto;
} else {
txData.setDestination(etAddress.getEditText().getText().toString());
ServiceHelper.ASSET = null;
ShiftService.ASSET = null;
}
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
txData.setPriority(PendingTransaction.Priority.Priority_Default);
@ -445,49 +399,33 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
public void processScannedData(BarcodeData barcodeData) {
barcodeData.filter(ShiftService.getPossibleAssets());
sendListener.setBarcodeData(barcodeData);
if (isResumed())
processScannedData();
}
public void processScannedData() {
BarcodeData barcodeData = sendListener.getBarcodeData();
final BarcodeData barcodeData = sendListener.getBarcodeData();
if (barcodeData != null) {
Timber.d("GOT DATA");
if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) {
if (!Helper.ALLOW_SHIFT && (barcodeData.getAsset() != Crypto.XMR)) {
Timber.d("BUT ONLY XMR SUPPORTED");
barcodeData = null;
sendListener.setBarcodeData(barcodeData);
sendListener.setBarcodeData(null);
return;
}
if (barcodeData.address != null) {
etAddress.getEditText().setText(barcodeData.address);
possibleCryptos.clear();
selectedCrypto = null;
if (barcodeData.isAmbiguous()) {
possibleCryptos.addAll(barcodeData.ambiguousAssets);
} else {
possibleCryptos.add(barcodeData.asset);
selectedCrypto = barcodeData.asset;
}
if (Helper.ALLOW_SHIFT)
updateCryptoButtons(false);
if (checkAddress()) {
if (barcodeData.security == BarcodeData.Security.OA_NO_DNSSEC)
etAddress.setError(getString(R.string.send_address_no_dnssec));
else if (barcodeData.security == BarcodeData.Security.OA_DNSSEC)
etAddress.setError(getString(R.string.send_address_openalias));
}
if (barcodeData.getAddress() != null) {
etAddress.getEditText().setText(barcodeData.getAddress());
} else {
etAddress.getEditText().getText().clear();
etAddress.setError(null);
}
String scannedNotes = barcodeData.addressName;
String scannedNotes = barcodeData.getAddressName();
if (scannedNotes == null) {
scannedNotes = barcodeData.description;
} else if (barcodeData.description != null) {
scannedNotes = scannedNotes + ": " + barcodeData.description;
scannedNotes = barcodeData.getDescription();
} else if (barcodeData.getDescription() != null) {
scannedNotes = scannedNotes + ": " + barcodeData.getDescription();
}
if (scannedNotes != null) {
etNotes.getEditText().setText(scannedNotes);

View File

@ -23,8 +23,6 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxData;
@ -38,17 +36,13 @@ public class SendAmountWizardFragment extends SendWizardFragment {
public static SendAmountWizardFragment newInstance(Listener listener) {
SendAmountWizardFragment instance = new SendAmountWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
private Listener sendListener;
public void setSendListener(Listener listener) {
this.sendListener = listener;
}
interface Listener {
public interface Listener {
SendFragment.Listener getActivityCallback();
TxData getTxData();
@ -139,7 +133,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.showKeyboard(getActivity());
Helper.showKeyboard(requireActivity());
final long funds = getTotalFunds();
maxFunds = 1.0 * funds / Helper.ONE_XMR;
if (!sendListener.getActivityCallback().isStreetMode()) {
@ -150,8 +144,8 @@ public class SendAmountWizardFragment extends SendWizardFragment {
getString(R.string.unknown_amount)));
}
final BarcodeData data = sendListener.popBarcodeData();
if ((data != null) && (data.amount != null)) {
etAmount.setAmount(data.amount);
if ((data != null) && (data.getAmount() != null)) {
etAmount.setAmount(data.getAmount());
}
}

View File

@ -17,48 +17,49 @@
package com.m2049r.xmrwallet.fragment.send;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.process.PreShiftProcess;
import com.m2049r.xmrwallet.util.AmountHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
import com.m2049r.xmrwallet.widget.SendProgressView;
import java.text.NumberFormat;
import java.util.Locale;
import timber.log.Timber;
public class SendBtcAmountWizardFragment extends SendWizardFragment {
public class SendBtcAmountWizardFragment extends SendWizardFragment implements PreShifter, ExchangeOtherEditText.Listener {
public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) {
SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment();
instance.setSendListener(listener);
return instance;
return new SendBtcAmountWizardFragment(listener);
}
SendAmountWizardFragment.Listener sendListener;
public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) {
this.sendListener = listener;
return this;
private SendBtcAmountWizardFragment(@NonNull SendAmountWizardFragment.Listener listener) {
super();
sendListener = listener;
}
private final SendAmountWizardFragment.Listener sendListener;
private TextView tvFunds;
private ExchangeOtherEditText etAmount;
@ -72,8 +73,6 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
sendListener = (SendAmountWizardFragment.Listener) getParentFragment();
View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false);
tvFunds = view.findViewById(R.id.tvFunds);
@ -83,6 +82,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
((ImageView) view.findViewById(R.id.shiftIcon)).setImageResource(service.getIconId());
etAmount = view.findViewById(R.id.etAmount);
etAmount.requestFocus();
@ -98,32 +99,19 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
if (orderParameters == null) {
return false; // this should never happen
}
if (sendListener != null) {
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
String btcString = etAmount.getNativeAmount();
if (btcString != null) {
try {
double btc = Double.parseDouble(btcString);
Timber.d("setBtcAmount %f", btc);
txDataBtc.setBtcAmount(btc);
txDataBtc.setAmount(btc / orderParameters.getPrice());
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
txDataBtc.setBtcAmount(0);
}
} else {
txDataBtc.setBtcAmount(0);
}
}
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
txDataBtc.setShiftAmount(etAmount.getPrimaryAmount());
return true;
}
double maxBtc = 0;
double minBtc = 0;
private double maxBtc = 0;
private double minBtc = 0;
@Override
public void onPauseFragment() {
super.onPauseFragment();
llXmrToParms.setVisibility(View.INVISIBLE);
etAmount.setListener(null);
}
@Override
@ -131,8 +119,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
super.onResumeFragment();
Timber.d("onResumeFragment()");
final String btcSymbol = ((TxDataBtc) sendListener.getTxData()).getBtcSymbol();
if (!btcSymbol.toLowerCase().equals(ServiceHelper.ASSET))
throw new IllegalStateException("Asset Symbol is wrong!");
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
final long funds = getTotalFunds();
if (!sendListener.getActivityCallback().isStreetMode()) {
tvFunds.setText(getString(R.string.send_available,
@ -143,48 +131,131 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
getString(R.string.unknown_amount)));
}
etAmount.setAmount("");
etAmount.setListener(this);
final BarcodeData data = sendListener.popBarcodeData();
if (data != null) {
if (data.amount != null) {
etAmount.setAmount(data.amount);
if (data.getAmount() != null) {
etAmount.setAmount(data.getAmount());
}
}
etAmount.setBaseCurrency(btcSymbol);
callXmrTo();
updateShift();
}
long getTotalFunds() {
return sendListener.getActivityCallback().getTotalFunds();
}
private final ShiftService service = ShiftService.DEFAULT;
private final PreShiftProcess preShiftProcess = service.createPreProcess(this);
private QueryOrderParameters orderParameters = null;
private void processOrderParms(final QueryOrderParameters orderParameters) {
private void reset() {
orderParameters = null;
maxBtc = 0;
minBtc = 0;
etAmount.setExchangeRate(0);
}
private void updateShift() {
reset();
getTxData().setShiftService(service);
llXmrToParms.setVisibility(View.INVISIBLE);
preShiftProcess.run();
}
private TxDataBtc getTxData() {
final TxData txData = sendListener.getTxData();
if (txData instanceof TxDataBtc) {
return (TxDataBtc) txData;
} else {
throw new IllegalStateException("TxData not BTC");
}
}
private boolean isValid() {
return orderParameters != null;
}
@Override
public CryptoAmount getAmount() { // of BTC
return etAmount.getPrimaryAmount();
}
public double getLowerLimit() {
if (!isValid()) throw new IllegalStateException();
return orderParameters.getLowerLimit();
}
public double getPrice() {
if (!isValid()) throw new IllegalStateException();
return orderParameters.getPrice();
}
public double getUpperLimit() {
if (!isValid()) throw new IllegalStateException();
return orderParameters.getUpperLimit();
}
@Override
public void onOrderParametersError(final Exception ex) {
reset();
Timber.e(ex);
requireView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
evParams.showMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evParams.setOnClickListener(v -> {
evParams.setOnClickListener(null);
updateShift();
});
} else {
evParams.showMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
});
}
@Override
public void onOrderParametersReceived(QueryOrderParameters orderParameters) {
final double price = orderParameters.getPrice();
maxBtc = price * orderParameters.getUpperLimit();
minBtc = price * orderParameters.getLowerLimit();
this.orderParameters = orderParameters;
getView().post(() -> {
final double price = orderParameters.getPrice();
requireView().post(() -> {
etAmount.setExchangeRate(1 / price);
maxBtc = price * orderParameters.getUpperLimit();
minBtc = price * orderParameters.getLowerLimit();
Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc);
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(6);
String min = df.format(minBtc);
String max = df.format(maxBtc);
String rate = df.format(price);
final String min = AmountHelper.format_6(minBtc);
final String max = AmountHelper.format_6(maxBtc);
final String rate = AmountHelper.format_6(price);
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
min, max, rate, txDataBtc.getBtcSymbol()));
final Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
min, max, rate, txDataBtc.getBtcSymbol(), service.getLabel()));
tvXmrToParms.setText(xmrParmText);
final long funds = getTotalFunds();
double availableXmr = 1.0 * funds / Helper.ONE_XMR;
final double availableXmr = 1.0 * funds / Helper.ONE_XMR;
String availBtcString;
String availXmrString;
if (!sendListener.getActivityCallback().isStreetMode()) {
availBtcString = df.format(availableXmr * price);
availXmrString = df.format(availableXmr);
availBtcString = AmountHelper.format_6(availableXmr * price);
availXmrString = AmountHelper.format_6(availableXmr);
} else {
availBtcString = getString(R.string.unknown_amount);
availXmrString = availBtcString;
@ -198,66 +269,28 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
});
}
private void processOrderParmsError(final Exception ex) {
etAmount.setExchangeRate(0);
orderParameters = null;
maxBtc = 0;
minBtc = 0;
Timber.e(ex);
getView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evParams.setOnClickListener(v -> {
evParams.setOnClickListener(null);
callXmrTo();
});
} else {
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
});
}
@Override
public boolean isActive() {
return true;
} // TODO Test what happens if we swtich away while querying
private void callXmrTo() {
@Override
public void showProgress() {
evParams.showProgress(getString(R.string.label_send_progress_queryparms));
getXmrToApi().queryOrderParameters(new ShiftCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameters) {
processOrderParms(orderParameters);
}
@Override
public void onError(final Exception e) {
processOrderParmsError(e);
}
});
}
private SideShiftApi xmrToApi = null;
long lastRequest = 0;
final static long EXCHANGE_TIME = 750; //ms
final Handler handler = new Handler(Looper.getMainLooper());
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
@Override
public void onExchangeRequested() {
final long now = System.currentTimeMillis();
lastRequest = now;
handler.postDelayed(() -> {
if (now == lastRequest) { // otherwise we are superseded
updateShift();
}
}
return xmrToApi;
}, EXCHANGE_TIME);
}
}

View File

@ -24,39 +24,36 @@ import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.process.ShiftProcess;
import com.m2049r.xmrwallet.util.AmountHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.widget.SendProgressView;
import java.text.NumberFormat;
import java.util.Locale;
import timber.log.Timber;
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm, Shifter {
public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) {
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
SendConfirmWizardFragment.Listener sendListener;
public void setSendListener(SendConfirmWizardFragment.Listener listener) {
this.sendListener = listener;
}
private SendConfirmWizardFragment.Listener sendListener;
private View llStageA;
private SendProgressView evStageA;
@ -77,11 +74,12 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
private TextView tvTxChange;
private View llPocketChange;
private TextView tvTxXmrToKeyLabel;
private TextView tvTxXmrToInfo;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
@ -93,6 +91,10 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate);
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
tvTxXmrToKeyLabel = view.findViewById(R.id.tvTxXmrToKeyLabel);
tvTxXmrToInfo = view.findViewById(R.id.tvTxXmrToInfo);
tvTxFee = view.findViewById(R.id.tvTxFee);
tvTxTotal = view.findViewById(R.id.tvTxTotal);
tvTxChange = view.findViewById(R.id.tvTxChange);
@ -106,7 +108,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
evStageC = view.findViewById(R.id.evStageC);
tvTxXmrToKey.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
});
@ -125,68 +127,74 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
return view;
}
int inProgress = 0;
final static int STAGE_X = 0;
final static int STAGE_A = 1;
final static int STAGE_B = 2;
final static int STAGE_C = 3;
@NonNull
Shifter.Stage inProgress = Stage.X;
private void showProgress(int stage, String progressText) {
Timber.d("showProgress(%d)", stage);
inProgress = stage;
switch (stage) {
case STAGE_A:
evStageA.showProgress(progressText);
break;
case STAGE_B:
evStageB.showProgress(progressText);
break;
case STAGE_C:
evStageC.showProgress(progressText);
break;
default:
throw new IllegalStateException("unknown stage " + stage);
}
@Override
public void showProgress(@NonNull Shifter.Stage stage) {
Timber.d("showProgress(%s)", stage);
requireView().post(() -> {
switch (stage) {
case A:
evStageA.showProgress(getString(R.string.label_send_progress_xmrto_create));
break;
case B:
evStageB.showProgress(getString(R.string.label_send_progress_xmrto_query));
break;
case C:
evStageC.showProgress(getString(R.string.label_send_progress_create_tx));
break;
default:
throw new IllegalArgumentException("invalid stage " + stage);
}
inProgress = stage;
});
}
public void hideProgress() {
Timber.d("hideProgress(%d)", inProgress);
Timber.d("hideProgress(%s)", inProgress);
switch (inProgress) {
case STAGE_A:
case A:
evStageA.hideProgress();
llStageA.setVisibility(View.VISIBLE);
break;
case STAGE_B:
case B:
evStageB.hideProgress();
llStageA.setVisibility(View.VISIBLE); // show Stage A info when B is ready
llStageB.setVisibility(View.VISIBLE);
break;
case STAGE_C:
case C:
evStageC.hideProgress();
llStageC.setVisibility(View.VISIBLE);
break;
default:
throw new IllegalStateException("unknown stage " + inProgress);
}
inProgress = STAGE_X;
inProgress = Stage.X;
}
public void showStageError(String code, String message, String solution) {
private void showErrorMessage(String code, String message, String solution) {
switch (inProgress) {
case STAGE_A:
case A:
evStageA.showMessage(code, message, solution);
break;
case STAGE_B:
case B:
evStageB.showMessage(code, message, solution);
break;
case STAGE_C:
case C:
evStageC.showMessage(code, message, solution);
break;
default:
throw new IllegalStateException("unknown stage");
throw new IllegalStateException("invalid stage");
}
inProgress = STAGE_X;
inProgress = Stage.X;
}
public void showQuoteError() {
showErrorMessage(ShiftError.Type.SERVICE.toString(),
getString(R.string.shift_noquote),
getString(R.string.shift_checkamount));
}
PendingTransaction pendingTransaction = null;
void send() {
@ -196,11 +204,8 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
return;
}
sendListener.getTxData().getUserNotes().setXmrtoOrder(xmrtoOrder); // note the transaction in the TX notes
((TxDataBtc) sendListener.getTxData()).setXmrtoOrderId(xmrtoOrder.getOrderId()); // remember the order id for later
// TODO make method in TxDataBtc to set both of the above in one go
sendListener.commitTransaction();
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
}
@Override
@ -209,15 +214,18 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_LONG).show();
}
private String orderId = null;
@Override
// callback from wallet when PendingTransaction created (started by prepareSend() here
public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
if (isResumed
&& (inProgress == STAGE_C)
&& (xmrtoOrder != null)
&& (xmrtoOrder.getOrderId().equals(txTag))) {
&& (inProgress == Stage.C)
&& (orderId != null)
&& (orderId.equals(txTag))) {
this.pendingTransaction = pendingTransaction;
getView().post(() -> {
requireView().post(() -> {
Timber.d("transactionCreated");
hideProgress();
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
tvTxTotal.setText(Wallet.getDisplayAmount(
@ -243,29 +251,35 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
if (pendingTransaction != null) {
throw new IllegalStateException("pendingTransaction is not null");
}
showStageError(getString(R.string.send_create_tx_error_title),
showErrorMessage(getString(R.string.send_create_tx_error_title),
errorText,
getString(R.string.text_noretry_monero));
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
public void onPauseFragment() {
isResumed = false;
shiftProcess = null;
stopSendTimer();
sendListener.disposeTransaction();
pendingTransaction = null;
inProgress = STAGE_X;
inProgress = Stage.X;
//TODO: maybe reset the progress messages
updateSendButton();
super.onPauseFragment();
}
private TxDataBtc getTxData() {
final TxData txData = sendListener.getTxData();
if (txData instanceof TxDataBtc) {
return (TxDataBtc) txData;
} else {
throw new IllegalStateException("TxData not BTC");
}
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
@ -273,8 +287,9 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
if (sendListener.getMode() != SendFragment.Mode.BTC) {
throw new IllegalStateException("Mode is not BTC!");
}
if (!((TxDataBtc) sendListener.getTxData()).getBtcSymbol().toLowerCase().equals(ServiceHelper.ASSET))
throw new IllegalStateException("Asset Symbol is wrong!");
final String btcSymbol = getTxData().getBtcSymbol();
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
Helper.hideKeyboard(getActivity());
llStageA.setVisibility(View.INVISIBLE);
evStageA.hideProgress();
@ -282,9 +297,17 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
evStageB.hideProgress();
llStageC.setVisibility(View.INVISIBLE);
evStageC.hideProgress();
if (shiftProcess != null) throw new IllegalStateException("shiftProcess not null");
shiftProcess = getTxData().getShiftService().createProcess(this);
tvTxXmrToKeyLabel.setText(getString(R.string.label_send_btc_xmrto_key, shiftProcess.getService().getLabel()));
tvTxXmrToKeyLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(shiftProcess.getService().getIconId(), 0, 0, 0);
tvTxXmrToInfo.setText(getString(R.string.label_send_btc_xmrto_info, shiftProcess.getService().getLabel()));
isResumed = true;
if ((pendingTransaction == null) && (inProgress == STAGE_X)) {
stageA();
if ((pendingTransaction == null) && (inProgress == Stage.X)) {
Timber.d("Starting ShiftProcess");
shiftProcess.run(getTxData());
} // otherwise just sit there blank
// TODO: don't sit there blank - can this happen? should we just die?
}
@ -310,19 +333,19 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
}
int minutes = sendCountdown / 60;
int seconds = sendCountdown % 60;
String t = String.format("%d:%02d", minutes, seconds);
String t = String.format(Locale.US, "%d:%02d", minutes, seconds);
bSend.setText(getString(R.string.send_send_timed_label, t));
if (sendCountdown > 0) {
sendCountdown -= XMRTO_COUNTDOWN_STEP;
getView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
requireView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
}
}
};
getView().post(updateRunnable);
requireView().post(updateRunnable);
}
void stopSendTimer() {
getView().removeCallbacks(updateRunnable);
requireView().removeCallbacks(updateRunnable);
}
void updateSendButton() {
@ -344,7 +367,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
}
public void fail(String walletName) {
getActivity().runOnUiThread(() -> {
requireActivity().runOnUiThread(() -> {
bSend.setEnabled(sendCountdown > 0); // allow to try again
});
}
@ -353,211 +376,127 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
// creates a pending transaction and calls us back with transactionCreated()
// or createTransactionFailed()
void prepareSend() {
public void prepareSend(CreateOrder order) {
if (!isResumed) return;
if ((xmrtoOrder == null)) {
throw new IllegalStateException("xmrtoOrder is null");
if ((order == null)) {
throw new IllegalStateException("order is null");
}
showProgress(3, getString(R.string.label_send_progress_create_tx));
final TxData txData = sendListener.getTxData();
txData.setDestination(xmrtoOrder.getXmrAddress());
txData.setAmount(xmrtoOrder.getXmrAmount());
getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData);
showProgress(Stage.C);
orderId = order.getOrderId();
final TxDataBtc txData = getTxData();
txData.setDestination(order.getXmrAddress());
txData.setAmount(order.getXmrAmount());
txData.getUserNotes().setXmrtoOrder(order); // note the transaction in the TX notes
txData.setXmrtoOrderId(order.getOrderId()); // remember the order id for later
txData.setXmrtoQueryOrderToken(order.getQueryOrderId()); // remember the order id for later
getActivityCallback().onPrepareSend(order.getOrderId(), txData);
}
SendFragment.Listener getActivityCallback() {
return sendListener.getActivityCallback();
}
private RequestQuote xmrtoQuote = null;
private ShiftProcess shiftProcess;
private void processStageA(final RequestQuote requestQuote) {
Timber.d("processCreateOrder %s", requestQuote.getId());
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
// verify the BTC amount is correct
if (requestQuote.getBtcAmount() != txDataBtc.getBtcAmount()) {
Timber.d("Failed to get quote");
getView().post(() -> showStageError(ShiftError.Error.SERVICE.toString(),
getString(R.string.shift_noquote),
getString(R.string.shift_checkamount)));
return; // just stop for now
}
xmrtoQuote = requestQuote;
txDataBtc.setAmount(xmrtoQuote.getXmrAmount());
getView().post(() -> {
// show data from the actual quote as that is what is used to
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
final String btcAmount = df.format(xmrtoQuote.getBtcAmount());
final String xmrAmountTotal = df.format(xmrtoQuote.getXmrAmount());
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
btcAmount, xmrAmountTotal, txDataBtc.getBtcSymbol()));
final String xmrPriceBtc = df.format(xmrtoQuote.getPrice());
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc, txDataBtc.getBtcSymbol()));
hideProgress();
});
stageB(requestQuote.getId());
public void showQuote(double btcAmount, double xmrAmount, double price) {
final String symbol = getTxData().getBtcSymbol();
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
AmountHelper.format(btcAmount), AmountHelper.format(xmrAmount), symbol));
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, AmountHelper.format(price), symbol));
}
private void processStageAError(final Exception ex) {
// Shifter
public void onQuoteReceived(RequestQuote quote) {
requireView().post(() -> {
Timber.d("onQuoteReceived");
showQuote(quote.getBtcAmount(), quote.getXmrAmount(), quote.getPrice());
hideProgress();
});
}
public void onQuoteError(final Exception ex) {
Timber.e("processStageAError %s", ex.getLocalizedMessage());
getView().post(() -> {
requireView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageA.setOnClickListener(v -> {
evStageA.setOnClickListener(null);
stageA();
shiftProcess.restart();
});
} else {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
showErrorMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
});
}
private void stageA() {
if (!isResumed) return;
Timber.d("Request Quote");
xmrtoQuote = null;
xmrtoOrder = null;
showProgress(1, getString(R.string.label_send_progress_xmrto_create));
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
@Override
public void onSuccess(RequestQuote requestQuote) {
if (!isResumed) return;
if (xmrtoQuote != null) {
Timber.w("another ongoing request quote request");
return;
}
processStageA(requestQuote);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
if (xmrtoQuote != null) {
Timber.w("another ongoing request quote request");
return;
}
processStageAError(ex);
}
};
getXmrToApi().requestQuote(txDataBtc.getBtcAmount(), callback);
@Override
public boolean isActive() {
return isResumed;
}
private CreateOrder xmrtoOrder = null;
private void processStageB(final CreateOrder order) {
Timber.d("processCreateOrder %s for %s", order.getOrderId(), order.getQuoteId());
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
// verify amount & destination
if ((order.getBtcAmount() != txDataBtc.getBtcAmount())
|| (!txDataBtc.validateAddress(order.getBtcAddress()))) {
throw new IllegalStateException("Order does not fulfill quote!"); // something is terribly wrong - die
}
xmrtoOrder = order;
getView().post(() -> {
public void onOrderCreated(CreateOrder order) {
requireView().post(() -> {
showQuote(order.getBtcAmount(), order.getXmrAmount(), order.getBtcAmount() / order.getXmrAmount());
tvTxXmrToKey.setText(order.getOrderId());
tvTxBtcAddress.setText(order.getBtcAddress());
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, txDataBtc.getBtcSymbol()));
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, order.getBtcCurrency()));
Timber.d("onOrderCreated");
hideProgress();
Timber.d("Expires @ %s", order.getExpiresAt().toString());
final int timeout = (int) (order.getExpiresAt().getTime() - order.getCreatedAt().getTime()) / 1000 - 60; // -1 minute buffer
startSendTimer(timeout);
prepareSend();
prepareSend(order);
});
}
private void processStageBError(final Exception ex) {
Timber.e("processCreateOrderError %s", ex.getLocalizedMessage());
getView().post(() -> {
public void onOrderError(final Exception ex) {
Timber.e("onOrderError %s", ex.getLocalizedMessage());
requireView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageB.setOnClickListener(v -> {
evStageB.setOnClickListener(null);
stageB(xmrtoOrder.getOrderId());
shiftProcess.retryCreateOrder();
});
} else {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(), null);
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
showErrorMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
});
}
private void stageB(final String quoteId) {
Timber.d("createOrder(%s)", quoteId);
if (!isResumed) return;
final String btcAddress = ((TxDataBtc) sendListener.getTxData()).getBtcAddress();
getView().post(() -> {
xmrtoOrder = null;
showProgress(2, getString(R.string.label_send_progress_xmrto_query));
getXmrToApi().createOrder(quoteId, btcAddress, new ShiftCallback<CreateOrder>() {
@Override
public void onSuccess(CreateOrder order) {
if (!isResumed) return;
if (xmrtoQuote == null) return;
if (!order.getQuoteId().equals(xmrtoQuote.getId())) {
Timber.d("Quote ID does not match");
// ignore (we got a response to a stale request)
return;
}
if (xmrtoOrder != null)
throw new IllegalStateException("xmrtoOrder must be null here!");
processStageB(order);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
processStageBError(ex);
}
});
});
}
private SideShiftApi xmrToApi = null;
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
@Override
public void invalidateShift() {
orderId = null;
}
}

View File

@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.fragment.send;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -32,16 +31,17 @@ import android.widget.Toast;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import java.net.SocketTimeoutException;
import java.text.NumberFormat;
import java.util.Locale;
@ -51,15 +51,11 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
SendSuccessWizardFragment.Listener sendListener;
public void setSendListener(SendSuccessWizardFragment.Listener listener) {
this.sendListener = listener;
}
private SendSuccessWizardFragment.Listener sendListener;
ImageButton bCopyTxId;
private TextView tvTxId;
@ -80,6 +76,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
final ShiftService shiftService = getTxData().getShiftService();
View view = inflater.inflate(
R.layout.fragment_send_btc_success, container, false);
@ -87,7 +84,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
bCopyTxId = view.findViewById(R.id.bCopyTxId);
bCopyTxId.setEnabled(false);
bCopyTxId.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
});
@ -105,23 +102,22 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
pbXmrto = view.findViewById(R.id.pbXmrto);
pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY);
final TextView tvXmrToLabel = view.findViewById(R.id.tvXmrToLabel);
tvXmrToLabel.setText(getString(R.string.info_send_xmrto_success_order_label, shiftService.getLabel()));
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
tvTxXmrToKey.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
});
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, shiftService.getLabel()));
return view;
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
@ -130,7 +126,15 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
super.onPauseFragment();
}
TxDataBtc btcData = null;
private TxDataBtc getTxData() {
final TxData txData = sendListener.getTxData();
if (txData == null) throw new IllegalStateException("TxDataBtc is null");
if (txData instanceof TxDataBtc) {
return (TxDataBtc) txData;
} else {
throw new IllegalStateException("TxData not BTC");
}
}
@Override
public void onResumeFragment() {
@ -139,8 +143,8 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
Helper.hideKeyboard(getActivity());
isResumed = true;
btcData = (TxDataBtc) sendListener.getTxData();
tvTxAddress.setText(btcData.getDestination());
final TxDataBtc txData = getTxData();
tvTxAddress.setText(txData.getDestination());
final PendingTx committedTx = sendListener.getCommittedTx();
if (committedTx != null) {
@ -148,45 +152,41 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
bCopyTxId.setEnabled(true);
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
if (btcData != null) {
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
String btcAmount = df.format(btcData.getBtcAmount());
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, btcData.getBtcSymbol()));
//TODO btcData.getBtcAddress();
tvTxXmrToKey.setText(btcData.getXmrtoOrderId());
final Crypto crypto = Crypto.withSymbol(btcData.getBtcSymbol());
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
tvXmrToSupport.setOnClickListener(v -> {
Uri orderUri = getXmrToApi().getQueryOrderUri(btcData.getXmrtoOrderId());
Intent intent = new Intent(Intent.ACTION_VIEW, orderUri);
startActivity(intent);
});
queryOrder();
} else {
throw new IllegalStateException("btcData is null");
}
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
String btcAmount = df.format(txData.getBtcAmount());
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, txData.getBtcSymbol()));
//TODO btcData.getBtcAddress();
tvTxXmrToKey.setText(txData.getXmrtoOrderId());
final Crypto crypto = Crypto.withSymbol(txData.getBtcSymbol());
assert crypto != null;
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
tvXmrToSupport.setOnClickListener(v -> {
startActivity(new Intent(Intent.ACTION_VIEW, txData.getShiftService().getShiftApi().getQueryOrderUri(txData.getXmrtoOrderId())));
});
queryOrder();
}
sendListener.enableDone();
}
private void processQueryOrder(final QueryOrderStatus status) {
Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getOrderId());
if (!btcData.getXmrtoOrderId().equals(status.getOrderId()))
Timber.d("processQueryOrder %s for %s", status.getStatus().toString(), status.getOrderId());
if (!getTxData().getXmrtoOrderId().equals(status.getOrderId()))
throw new IllegalStateException("UUIDs do not match!");
if (isResumed && (getView() != null))
getView().post(() -> {
showXmrToStatus(status);
if (!status.isTerminal()) {
getView().postDelayed(this::queryOrder, SideShiftApi.QUERY_INTERVAL);
getView().postDelayed(this::queryOrder, ShiftApi.QUERY_INTERVAL);
}
});
}
private void queryOrder() {
final TxDataBtc btcData = getTxData();
Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId());
if (!isResumed) return;
getXmrToApi().queryOrderStatus(btcData.getXmrtoOrderId(), new ShiftCallback<QueryOrderStatus>() {
btcData.getShiftService().getShiftApi().queryOrderStatus(btcData.getXmrtoQueryOrderToken(), new ShiftCallback<QueryOrderStatus>() {
@Override
public void onSuccess(QueryOrderStatus status) {
if (!isAdded()) return;
@ -197,45 +197,54 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
public void onError(final Exception ex) {
if (!isResumed) return;
Timber.w(ex);
getActivity().runOnUiThread(() -> {
if (ex instanceof ShiftException) {
Toast.makeText(getActivity(), ((ShiftException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
});
if (ex instanceof SocketTimeoutException) {
// try again
if (isResumed && (getView() != null))
getView().post(() -> {
getView().postDelayed(SendBtcSuccessWizardFragment.this::queryOrder, ShiftApi.QUERY_INTERVAL);
});
} else {
requireActivity().runOnUiThread(() -> {
if (ex instanceof ShiftException) {
Toast.makeText(getActivity(), ((ShiftException) ex).getErrorMessage(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
});
}
void showXmrToStatus(final QueryOrderStatus status) {
int statusResource = 0;
final TxDataBtc txData = getTxData();
if (status.isError()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString()));
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, txData.getShiftService().getLabel(), status.toString()));
statusResource = R.drawable.ic_error_red_24dp;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError),
ThemeHelper.getThemedColor(requireContext(), R.attr.negativeColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isSent() || status.isPaid()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, btcData.getBtcSymbol()));
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, txData.getBtcSymbol()));
statusResource = R.drawable.ic_success;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor),
ThemeHelper.getThemedColor(requireContext(), R.attr.positiveColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isWaiting()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
statusResource = R.drawable.ic_pending;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isPending()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
statusResource = R.drawable.ic_pending;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else {
throw new IllegalStateException("status is broken: " + status.toString());
throw new IllegalStateException("status is broken: " + status);
}
ivXmrToStatus.setImageResource(statusResource);
if (status.isTerminal()) {
@ -246,17 +255,4 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
ivXmrToStatusBig.setVisibility(View.VISIBLE);
}
}
private SideShiftApi xmrToApi = null;
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

View File

@ -38,18 +38,13 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
public static SendConfirmWizardFragment newInstance(Listener listener) {
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
private Listener sendListener;
public SendConfirmWizardFragment setSendListener(Listener listener) {
this.sendListener = listener;
return this;
}
interface Listener {
public interface Listener {
SendFragment.Listener getActivityCallback();
TxData getTxData();
@ -137,7 +132,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
void send() {
sendListener.commitTransaction();
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
}
@Override
@ -153,7 +148,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
}
private void showAlert(String title, String message) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity());
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
builder.setCancelable(true).
setTitle(title).
setMessage(message).
@ -235,7 +230,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
}
public void fail(String walletName) {
getActivity().runOnUiThread(() -> {
requireActivity().runOnUiThread(() -> {
bSend.setEnabled(true); // allow to try again
});
}

View File

@ -56,6 +56,7 @@ import com.m2049r.xmrwallet.widget.Toolbar;
import java.lang.ref.WeakReference;
import lombok.Getter;
import timber.log.Timber;
public class SendFragment extends Fragment
@ -67,6 +68,7 @@ public class SendFragment extends Fragment
final static public int MIXIN = 0;
@Getter
private Listener activityCallback;
public interface Listener {
@ -273,7 +275,7 @@ public class SendFragment extends Fragment
return false;
}
enum Mode {
public enum Mode {
XMR, BTC
}
@ -293,7 +295,7 @@ public class SendFragment extends Fragment
default:
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
}
getView().post(() -> pagerAdapter.notifyDataSetChanged());
requireView().post(() -> pagerAdapter.notifyDataSetChanged());
Timber.d("New Mode = %s", mode.toString());
}
}
@ -478,11 +480,6 @@ public class SendFragment extends Fragment
bDone.setVisibility(View.VISIBLE);
}
public Listener getActivityCallback() {
return activityCallback;
}
// callbacks from send service
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
@ -502,12 +499,9 @@ public class SendFragment extends Fragment
activityCallback.onDisposeRequest();
}
@Getter
PendingTx pendingTx;
public PendingTx getPendingTx() {
return pendingTx;
}
public void onCreateTransactionFailed(String errorText) {
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {

View File

@ -36,18 +36,13 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
public static SendSuccessWizardFragment newInstance(Listener listener) {
SendSuccessWizardFragment instance = new SendSuccessWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
private Listener sendListener;
public SendSuccessWizardFragment setSendListener(Listener listener) {
this.sendListener = listener;
return this;
}
interface Listener {
public interface Listener {
TxData getTxData();
PendingTx getCommittedTx();
@ -62,7 +57,6 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
ImageButton bCopyTxId;
private TextView tvTxId;
private TextView tvTxAddress;
private TextView tvTxPaymentId;
private TextView tvTxAmount;
private TextView tvTxFee;
@ -78,13 +72,12 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
bCopyTxId = view.findViewById(R.id.bCopyTxId);
bCopyTxId.setEnabled(false);
bCopyTxId.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
});
tvTxId = view.findViewById(R.id.tvTxId);
tvTxAddress = view.findViewById(R.id.tvTxAddress);
tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId);
tvTxAmount = view.findViewById(R.id.tvTxAmount);
tvTxFee = view.findViewById(R.id.tvTxFee);

View File

@ -0,0 +1,26 @@
package com.m2049r.xmrwallet.fragment.send;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
public interface Shifter {
void invalidateShift();
void onQuoteError(final Exception ex);
void showQuoteError();
void onQuoteReceived(RequestQuote quote);
void onOrderCreated(CreateOrder order);
void onOrderError(final Exception ex);
boolean isActive();
void showProgress(Shifter.Stage stage);
enum Stage {
X, A, B, C
}
}

View File

@ -16,6 +16,7 @@
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -107,6 +108,7 @@ public class BluetoothInfoAdapter extends RecyclerView.Adapter<BluetoothInfoAdap
private boolean itemsClickable = true;
@SuppressLint("NotifyDataSetChanged")
public void allowClick(boolean clickable) {
itemsClickable = clickable;
notifyDataSetChanged();

View File

@ -16,6 +16,7 @@
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@ -131,12 +132,13 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
private boolean itemsClickable = true;
@SuppressLint("NotifyDataSetChanged")
public void allowClick(boolean clickable) {
itemsClickable = clickable;
notifyDataSetChanged();
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
final ImageButton ibBookmark;
final View pbBookmark;
final TextView tvName;
@ -195,7 +197,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
final NodeInfo node = nodeItems.get(position);
if (node.isOnion()) {
@ -218,7 +220,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
@Override
public boolean onLongClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
return listener.onLongInteraction(view, nodeItems.get(position));
}

View File

@ -89,6 +89,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
return items.size();
}
@NonNull
public Subaddress getItem(int position) {
return items.get(position);
}
@ -108,7 +109,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
diffResult.dispatchUpdatesTo(this);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
final TextView tvName;
final TextView tvAddress;
final TextView tvAmount;
@ -143,7 +144,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
listener.onInteraction(view, getItem(position));
}
@ -153,7 +154,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
@Override
public boolean onLongClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
return listener.onLongInteraction(view, getItem(position));
}

View File

@ -16,6 +16,7 @@
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Html;
import android.text.Spanned;
@ -49,6 +50,7 @@ import java.util.TimeZone;
import timber.log.Timber;
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
@SuppressLint("SimpleDateFormat")
private final static SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private final int outboundColour;
@ -156,7 +158,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
return infoItems.get(position);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final ImageView ivTxType;
final TextView tvAmount;
final TextView tvFailed;
@ -291,7 +293,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
listener.onInteraction(view, infoItems.get(position));
}

View File

@ -16,6 +16,7 @@
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.MenuItem;
@ -44,6 +45,7 @@ import timber.log.Timber;
public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.ViewHolder> {
@SuppressLint("SimpleDateFormat")
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public interface OnInteractionListener {
@ -120,7 +122,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
diffResult.dispatchUpdatesTo(this);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView tvName;
final ImageButton ibOptions;
WalletManager.WalletInfo infoItem;
@ -164,7 +166,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
listener.onInteraction(view, infoItems.get(position));
}

View File

@ -142,9 +142,13 @@ public class ExchangeApiImpl implements ExchangeApi {
} catch (ParserConfigurationException | SAXException ex) {
Timber.w(ex);
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
} finally {
response.close();
}
} else {
callback.onError(new ExchangeException(response.code(), response.message()));
final ExchangeException ex = new ExchangeException(response.code(), response.message());
response.close();
callback.onError(ex);
}
}
});

View File

@ -89,12 +89,12 @@ public class ExchangeApiImpl implements ExchangeApi {
final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url);
httpRequest.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(final Call call, final IOException ex) {
public void onFailure(@NonNull final Call call, @NonNull final IOException ex) {
callback.onError(ex);
}
@Override
public void onResponse(final Call call, final Response response) throws IOException {
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
if (response.isSuccessful()) {
try {
final JSONObject json = new JSONObject(response.body().string());
@ -108,9 +108,13 @@ public class ExchangeApiImpl implements ExchangeApi {
}
} catch (JSONException ex) {
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
} finally {
response.close();
}
} else {
callback.onError(new ExchangeException(response.code(), response.message()));
final ExchangeException ex = new ExchangeException(response.code(), response.message());
response.close();
callback.onError(ex);
}
}
});

View File

@ -95,9 +95,13 @@ public class ExchangeApiImpl implements ExchangeApi {
callback.onSuccess(new ExchangeRateImpl(quoteCurrency, rate, timestamp));
} catch (JSONException ex) {
callback.onError(ex);
} finally {
response.close();
}
} else {
callback.onError(new ExchangeException(response.code(), response.message()));
final ExchangeException ex = new ExchangeException(response.code(), response.message());
response.close();
callback.onError(ex);
}
}
});

View File

@ -22,6 +22,5 @@ public interface NetworkCallback {
void onSuccess(JSONObject jsonObject);
void onError(Exception ex);
void onError(Exception ex, JSONObject json);
}

View File

@ -21,8 +21,7 @@ import androidx.annotation.NonNull;
import org.json.JSONObject;
public interface ShiftApiCall {
void get(@NonNull final String path, final String parameters, @NonNull final NetworkCallback callback);
void call(@NonNull final String path, @NonNull final NetworkCallback callback);
void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback);
void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback);
}

View File

@ -18,32 +18,23 @@ package com.m2049r.xmrwallet.service.shift;
import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ShiftError {
@Getter
private final Error errorType;
private final Type type;
@Getter
private final String errorMsg;
public enum Error {
public enum Type {
SERVICE,
INFRASTRUCTURE
}
public boolean isRetryable() {
return errorType == Error.INFRASTRUCTURE;
}
public ShiftError(final JSONObject jsonObject) throws JSONException {
final JSONObject errorObject = jsonObject.getJSONObject("error");
errorType = Error.SERVICE;
errorMsg = errorObject.getString("message");
return type == Type.INFRASTRUCTURE;
}
@Override

View File

@ -16,6 +16,8 @@
package com.m2049r.xmrwallet.service.shift;
import androidx.annotation.Nullable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@ -24,10 +26,14 @@ public class ShiftException extends Exception {
@Getter
private final int code;
@Getter
@Nullable
private final ShiftError error;
public ShiftException(int code) {
this.code = code;
this.error = null;
this(code, null);
}
public String getErrorMessage() {
return (error != null) ? error.getErrorMsg() : ("HTTP:" + code);
}
}

View File

@ -0,0 +1,100 @@
package com.m2049r.xmrwallet.service.shift;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.fragment.send.PreShifter;
import com.m2049r.xmrwallet.fragment.send.Shifter;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.service.shift.process.PreProcess;
import com.m2049r.xmrwallet.service.shift.process.PreShiftProcess;
import com.m2049r.xmrwallet.service.shift.process.Process;
import com.m2049r.xmrwallet.service.shift.process.ShiftProcess;
import com.m2049r.xmrwallet.service.shift.provider.exolix.ExolixApiImpl;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum ShiftService {
XMRTO(false, "xmr.to", "xmrto", null, null, 0, R.drawable.ic_xmrto_logo, ""),
SIDESHIFT(false, "SideShift.ai", "side", null, null, R.drawable.ic_sideshift_icon, R.drawable.ic_sideshift_wide, ""),
EXOLIX(true, "EXOLIX", "exolix", new ExolixApiImpl(), Type.ONESTEP, R.drawable.ic_exolix_icon, R.drawable.ic_exolix_wide, "XMR:BTC:LTC:ETH:USDT:SOL"),
UNKNOWN(false, "", null, null, null, 0, 0, "");
static final public ShiftService DEFAULT = EXOLIX;
final private boolean enabled;
final private String label;
final private String tag;
final private ShiftApi shiftApi;
final private Type type;
final private int iconId;
final private int logoId;
final private String assets;
@NonNull
static public ShiftService findWithTag(String tag) {
if (tag == null) return UNKNOWN;
for (ShiftService service : values()) {
if (tag.equals(service.tag)) return service;
}
return UNKNOWN;
}
@Getter
static private final Set<Crypto> possibleAssets = new HashSet<>();
static {
assert DEFAULT.enabled;
for (ShiftService service : values()) {
if (!service.enabled) continue;
final String[] assets = service.getAssets().split(":");
for (String anAsset : assets) {
possibleAssets.add(Crypto.withSymbol(anAsset));
}
}
}
public static boolean isAssetSupported(@NonNull Crypto crypto) {
return possibleAssets.contains(crypto);
}
public static boolean isAssetSupported(@NonNull String symbol) {
final Crypto crypto = Crypto.withSymbol(symbol);
if (crypto != null) {
return isAssetSupported(crypto);
}
return false;
}
public boolean supportsAsset(@NonNull Crypto crypto) {
return assets.contains(crypto.getSymbol());
}
public boolean supportsAsset(@NonNull String symbol) {
final Crypto crypto = Crypto.withSymbol(symbol);
if (crypto != null) {
return supportsAsset(crypto);
}
return false;
}
public ShiftProcess createProcess(Shifter shifter) {
return new Process(this, shifter);
}
public PreShiftProcess createPreProcess(PreShifter shifter) {
return new PreProcess(this, shifter);
}
public enum Type {
ONESTEP, TWOSTEP;
}
public static Crypto ASSET = null; // keep asset to exchange globally
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 m2049r
*
* 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.service.shift;
public enum ShiftType {
FIXED, FLOAT;
}

View File

@ -14,12 +14,14 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import java.util.Date;
public interface CreateOrder {
String TAG = "side";
String getTag();
String getBtcCurrency();
@ -35,8 +37,11 @@ public interface CreateOrder {
String getXmrAddress();
Date getCreatedAt(); // createdAt
Date getCreatedAt();
Date getExpiresAt(); // expiresAt
Date getExpiresAt();
String getQueryOrderId();
ShiftType getType();
}

View File

@ -14,14 +14,14 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
public interface QueryOrderParameters {
double getLowerLimit();
double getLowerLimit(); // XMR
double getPrice();
double getPrice(); // BTC/XMR
double getUpperLimit();
double getUpperLimit(); // XMR
}

View File

@ -0,0 +1,106 @@
/*
* Copyright (c) 2017-2021 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.service.shift.api;
import androidx.annotation.Nullable;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class QueryOrderStatus {
final String orderId;
final Status status;
final String btcCurrency;
final double btcAmount;
final String btcAddress;
final double xmrAmount;
final String xmrAddress;
@Nullable
final Date createdAt;
@Nullable
final Date expiresAt;
public double getPrice() {
return btcAmount / xmrAmount;
}
public boolean isError() {
return status.isError();
}
public boolean isTerminal() {
return status.isTerminal();
}
public boolean isWaiting() {
return status.isWaiting();
}
public boolean isPending() {
return status.isPending();
}
public boolean isSent() {
return status.isSent();
}
public boolean isPaid() {
return status.isPaid();
}
public enum Status {
WAITING, // Waiting for mempool
PENDING, // Detected (waiting for confirmations)
SETTLING, // Settlement in progress
SETTLED, // Settlement completed
// no refunding in monerujo so these are ignored:
// REFUND, // Queued for refund
// REFUNDING, // Refund in progress
// REFUNDED // Refund completed
UNDEFINED,
EXPIRED,
ERROR; // Something went wrong and the user needs to interact with the provider
public boolean isError() {
return this == Status.UNDEFINED || this == Status.ERROR || this == Status.EXPIRED;
}
public boolean isTerminal() {
return (this == Status.SETTLED) || isError();
}
public boolean isWaiting() {
return this == Status.WAITING;
}
public boolean isPending() {
return this == Status.PENDING;
}
public boolean isSent() {
return this == Status.SETTLING;
}
public boolean isPaid() {
return this == Status.SETTLED;
}
}
}

View File

@ -14,13 +14,17 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import java.util.Date;
public interface RequestQuote {
double getBtcAmount(); // settleAmount
double getBtcAmount(); // what we want to receive
double getXmrAmount(); // the XMR we need to send
String getId(); // id
@ -28,7 +32,7 @@ public interface RequestQuote {
Date getExpiresAt(); // expiresAt
double getXmrAmount(); // depositAmount
double getPrice(); // rate
ShiftType getType();
}

View File

@ -14,15 +14,17 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
public interface SideShiftApi {
public interface ShiftApi {
int QUERY_INTERVAL = 5000; // ms
/**
@ -30,22 +32,23 @@ public interface SideShiftApi {
*
* @param callback the callback with the OrderParameter object
*/
void queryOrderParameters(@NonNull final ShiftCallback<QueryOrderParameters> callback);
void queryOrderParameters(@NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback);
/**
* Creates an order
*
* @param xmrAmount the desired XMR amount
* @param btcAddress destination
* @param btcAmount the desired amount to send
*/
void requestQuote(final double xmrAmount, @NonNull final ShiftCallback<RequestQuote> callback);
void requestQuote(@Nullable final String btcAddress, @NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<RequestQuote> callback);
/**
* Creates an order
*
* @param quoteId the desired XMR amount
* @param quote the quote from {@link #requestQuote(String, CryptoAmount, ShiftCallback)}
* @param btcAddress the target bitcoin address
*/
void createOrder(final String quoteId, @NonNull final String btcAddress, @NonNull final ShiftCallback<CreateOrder> callback);
void createOrder(final RequestQuote quote, @NonNull final String btcAddress, @NonNull final ShiftCallback<CreateOrder> callback);
/**
* Queries the order status for given current order

View File

@ -0,0 +1,53 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.fragment.send.PreShifter;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
@RequiredArgsConstructor
public class PreProcess implements PreShiftProcess {
@Getter
final private ShiftService service;
final private PreShifter preshifter;
@Override
public void run() {
getOrderParameters();
}
@Override
public void restart() {
run();
}
private void getOrderParameters() {
Timber.d("getOrderParameters");
if (!preshifter.isActive()) return;
preshifter.showProgress();
final CryptoAmount btcAmount = preshifter.getAmount();
service.getShiftApi().queryOrderParameters(btcAmount, new ShiftCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameters) {
if (!preshifter.isActive()) return;
onOrderParametersReceived(orderParameters);
}
@Override
public void onError(final Exception ex) {
if (!preshifter.isActive()) return;
preshifter.onOrderParametersError(ex);
}
});
}
private void onOrderParametersReceived(final QueryOrderParameters orderParameters) {
Timber.d("onOrderParmsReceived %f", orderParameters.getPrice());
preshifter.onOrderParametersReceived(orderParameters);
}
}

View File

@ -0,0 +1,11 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.service.shift.ShiftService;
public interface PreShiftProcess {
ShiftService getService();
void run();
void restart();
}

View File

@ -0,0 +1,178 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.fragment.send.Shifter;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import java.util.Date;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
@RequiredArgsConstructor
public class Process implements ShiftProcess {
@Getter
final private ShiftService service;
final private Shifter shifter;
private TxDataBtc txDataBtc;
private RequestQuote quote = null;
private CreateOrder order = null;
@Override
public void run(TxDataBtc txData) {
txDataBtc = txData;
switch (service.getType()) {
case TWOSTEP:
getQuote();
break;
case ONESTEP:
quote = new RequestQuote() {
@Override
public double getBtcAmount() {
return txDataBtc.getBtcAmount();
}
@Override
public double getXmrAmount() {
return txDataBtc.getXmrAmount();
}
@Override
public String getId() {
return null;
}
@Override
public Date getCreatedAt() {
return null;
}
@Override
public Date getExpiresAt() {
return null;
}
@Override
public double getPrice() {
return 0;
}
@Override
public ShiftType getType() {
return (txDataBtc.getShiftAmount().getCrypto() == Crypto.XMR) ? ShiftType.FLOAT : ShiftType.FIXED;
}
};
createOrder();
break;
default:
throw new IllegalStateException();
}
}
@Override
public void restart() {
run(txDataBtc);
}
private void getQuote() {
shifter.invalidateShift();
if (!shifter.isActive()) return;
Timber.d("Request Quote");
quote = null;
order = null;
shifter.showProgress(Shifter.Stage.A);
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
@Override
public void onSuccess(RequestQuote requestQuote) {
if (!shifter.isActive()) return;
if (quote != null) {
Timber.w("another ongoing request quote request");
return;
}
onQuoteReceived(requestQuote);
}
@Override
public void onError(Exception ex) {
if (!shifter.isActive()) return;
if (quote != null) {
Timber.w("another ongoing request quote request");
return;
}
shifter.onQuoteError(ex);
}
};
service.getShiftApi().requestQuote(txDataBtc.getBtcAddress(), txDataBtc.getShiftAmount(), callback);
}
private void onQuoteReceived(final RequestQuote quote) {
Timber.d("onQuoteReceived %s", quote.getId());
// verify the shift is correct
if (!txDataBtc.validate(quote)) {
Timber.d("Failed to get quote");
shifter.showQuoteError();
return; // just stop for now
}
this.quote = quote;
txDataBtc.setAmount(this.quote.getXmrAmount());
shifter.onQuoteReceived(this.quote);
createOrder();
}
@Override
public void retryCreateOrder() {
createOrder();
}
private void createOrder() {
Timber.d("createOrder(%s)", quote.getId());
if (!shifter.isActive()) return;
final String btcAddress = txDataBtc.getBtcAddress();
order = null;
shifter.showProgress(Shifter.Stage.B);
service.getShiftApi().createOrder(quote, btcAddress, new ShiftCallback<CreateOrder>() {
@Override
public void onSuccess(final CreateOrder order) {
if (!shifter.isActive()) return;
if (quote == null) return;
if ((quote.getId() != null) && !order.getQuoteId().equals(quote.getId())) {
Timber.d("Quote ID does not match");
// ignore (we got a response to a stale request)
return;
}
if (Process.this.order != null)
throw new IllegalStateException("order must be null here!");
onOrderReceived(order);
}
@Override
public void onError(final Exception ex) {
if (!shifter.isActive()) return;
shifter.onOrderError(ex);
}
});
}
private void onOrderReceived(final CreateOrder order) {
Timber.d("onOrderReceived %s for %s", order.getOrderId(), order.getQuoteId());
// verify amount & destination
if (!order.getBtcCurrency().equalsIgnoreCase(txDataBtc.getBtcSymbol()))
throw new IllegalStateException("Destination Currency is wrong: " + order.getBtcCurrency()); // something is terribly wrong - die
if ((order.getType() == ShiftType.FIXED) && (order.getBtcAmount() != txDataBtc.getShiftAmount().getAmount()))
throw new IllegalStateException("Destination Amount is wrong: " + order.getBtcAmount()); // something is terribly wrong - die
if ((order.getType() == ShiftType.FLOAT) && (order.getXmrAmount() != txDataBtc.getShiftAmount().getAmount()))
throw new IllegalStateException("Source Amount is wrong: " + order.getXmrAmount()); // something is terribly wrong - die
if (!txDataBtc.validateAddress(order.getBtcAddress())) {
throw new IllegalStateException("Destination address is wrong: " + order.getBtcAddress()); // something is terribly wrong - die
}
this.order = order;
shifter.onOrderCreated(order);
}
}

View File

@ -0,0 +1,14 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.service.shift.ShiftService;
public interface ShiftProcess {
ShiftService getService();
void run(TxDataBtc txData);
void restart();
void retryCreateOrder();
}

View File

@ -0,0 +1,165 @@
/*
* Copyright (c) 2017-2021 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.service.shift.provider;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.Response;
import timber.log.Timber;
abstract public class ShiftApiImpl implements ShiftApi, ShiftApiCall {
protected abstract String getBaseUrl();
protected abstract String getApiUrl();
private final HttpUrl api;
public ShiftApiImpl() {
api = HttpUrl.parse(getApiUrl());
}
@Override
public void queryOrderParameters(CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback) {
throw new UnsupportedOperationException();
}
@Override
public void requestQuote(@Nullable final String btcAddress, @NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
throw new UnsupportedOperationException();
}
@Override
public void createOrder(@NonNull final RequestQuote quote, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
throw new UnsupportedOperationException();
}
@Override
public void queryOrderStatus(@NonNull final String uuid,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
throw new UnsupportedOperationException();
}
@Override
public Uri getQueryOrderUri(String orderId) {
throw new UnsupportedOperationException();
}
// void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback);
@Override
public void get(@NonNull final String path, final String parameters, @NonNull final NetworkCallback callback) {
Timber.d("GET parameters=%s", parameters);
final HttpUrl.Builder builder = api.newBuilder().addPathSegments(path);
if (parameters != null)
for (String parm : parameters.split("&")) {
String[] p = parm.split("=");
builder.addQueryParameter(p[0], p[1]);
}
NetCipherHelper.Request request = new NetCipherHelper.Request(builder.build());
augment(request, null);
enqueue(request, callback);
}
@Override
public void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback) {
Timber.d("data=%s", data);
final HttpUrl url = api.newBuilder().addPathSegments(path).build();
final NetCipherHelper.Request request = new NetCipherHelper.Request(url, data);
augment(request, data);
enqueue(request, callback);
}
protected void augment(@NonNull final NetCipherHelper.Request request, @Nullable final JSONObject data) {
}
private void enqueue(NetCipherHelper.Request request, @NonNull final NetworkCallback callback) {
Timber.d("REQ: %s", request);
request.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@NonNull final Call call, @NonNull final IOException ex) {
callback.onError(ex, null);
}
@Override
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
Timber.d("onResponse code=%d", response.code());
try {
if (response.body() == null) {
callback.onError(new IllegalStateException("Empty response from service"), null);
return;
}
final String body = response.body().string();
if ((response.code() >= 200) && (response.code() <= 499)) {
try {
Timber.d(" SUCCESS %s", body);
final JSONObject json = new JSONObject(body);
final ShiftError error = createShiftError(ShiftError.Type.SERVICE, json);
if (error != null) {
callback.onError(new ShiftException(response.code(), error), json);
} else {
callback.onSuccess(json);
}
} catch (JSONException ex) {
callback.onError(ex, null);
}
} else {
try {
Timber.d("!SUCCESS %s", body);
final JSONObject json = new JSONObject(body);
Timber.d(json.toString(2));
final ShiftError error = createShiftError(ShiftError.Type.INFRASTRUCTURE, json);
Timber.d("%s says %d/%s", getBaseUrl(), response.code(), error);
callback.onError(new ShiftException(response.code(), error), json);
} catch (JSONException ex) {
callback.onError(new ShiftException(response.code()), null);
}
}
} finally {
response.close();
}
}
});
}
abstract protected ShiftError createShiftError(ShiftError.Type type, final JSONObject jsonObject);
}

View File

@ -0,0 +1,137 @@
/*
* Copyright (c) 2017-2021 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.service.shift.provider.exolix;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.util.DateHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
import lombok.Getter;
@Getter
class CreateOrderImpl implements CreateOrder {
private final static long EXPIRE = 10 * 60 * 1000; // 10 minutes
private final String btcCurrency;
private final double btcAmount;
private final String btcAddress;
private final String orderId;
private final double xmrAmount;
private final String xmrAddress;
private final Date createdAt;
private final Date expiresAt;
private final ShiftType type;
@Override
public String getQueryOrderId() {
return orderId;
}
@Override
public String getQuoteId() {
return null;
}
CreateOrderImpl(final JSONObject jsonObject) throws JSONException {
final JSONObject coinFrom = jsonObject.getJSONObject("coinFrom");
final JSONObject coinTo = jsonObject.getJSONObject("coinTo");
// sanity checks
final String depositMethod = coinFrom.getString("coinCode");
final String settleMethod = coinTo.getString("coinCode");
if (!"xmr".equalsIgnoreCase(depositMethod)
|| !ShiftService.ASSET.getSymbol().equalsIgnoreCase(settleMethod))
throw new IllegalStateException();
btcCurrency = settleMethod.toUpperCase();
btcAmount = jsonObject.getDouble("amountTo");
btcAddress = jsonObject.getString("withdrawalAddress");
xmrAmount = jsonObject.getDouble("amount");
xmrAddress = jsonObject.getString("depositAddress");
orderId = jsonObject.getString("id");
try {
final String created = jsonObject.getString("createdAt");
createdAt = DateHelper.parse(created);
expiresAt = new Date(createdAt.getTime() + EXPIRE);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
type = jsonObject.getString("rateType").equals("float") ? ShiftType.FLOAT : ShiftType.FIXED;
}
public static void call(@NonNull final ShiftApiCall api,
@NonNull final String btcAddress,
@NonNull final RequestQuote quote,
@NonNull final ShiftCallback<CreateOrder> callback) {
try {
final JSONObject request = createRequest(btcAddress, quote);
api.post("transactions", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new CreateOrderImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex, JSONObject json) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
static JSONObject createRequest(@NonNull final String btcAddress, @NonNull final RequestQuote quote) throws JSONException {
final JSONObject jsonObject = new JSONObject();
if (quote.getType() == ShiftType.FLOAT) {
jsonObject.put("rateType", "float");
jsonObject.put("amount", quote.getXmrAmount());
} else { // default is FIXED
jsonObject.put("rateType", "fixed");
jsonObject.put("withdrawalAmount", quote.getBtcAmount());
}
jsonObject.put("coinFrom", "XMR");
jsonObject.put("coinTo", ShiftService.ASSET.getSymbol());
jsonObject.put("networkTo", ShiftService.ASSET.getNetwork());
jsonObject.put("withdrawalAddress", btcAddress);
return jsonObject;
}
@Override
public String getTag() {
return ShiftService.EXOLIX.getTag();
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2017-2021 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.service.shift.provider.exolix;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.provider.ShiftApiImpl;
import com.m2049r.xmrwallet.util.IdHelper;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import org.json.JSONException;
import org.json.JSONObject;
import lombok.Getter;
public class ExolixApiImpl extends ShiftApiImpl {
@Getter
final private String baseUrl = "https://exolix.com";
@Getter
final private String apiUrl = baseUrl + "/api/v2";
@Override
public void queryOrderParameters(CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback) {
QueryOrderParametersImpl.call(this, btcAmount, callback);
}
@Override
public void createOrder(@NonNull final RequestQuote quote, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
CreateOrderImpl.call(this, btcAddress, quote, callback);
}
@Override
public void queryOrderStatus(@NonNull final String uuid,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
QueryOrderStatusImpl.call(this, uuid, callback);
}
@Override
public Uri getQueryOrderUri(String orderId) {
return Uri.parse(getBaseUrl() + "/transaction/" + orderId);
}
@Override
protected ShiftError createShiftError(ShiftError.Type type, final JSONObject jsonObject) {
try {
if (jsonObject.has("message")) {
final String message = jsonObject.getString("message");
if (!"null".equals(message))
return new ShiftError(type, message);
}
} catch (JSONException ex) {
return new ShiftError(ShiftError.Type.INFRASTRUCTURE, "unknown");
}
return null;
}
@Override
protected void augment(@NonNull final NetCipherHelper.Request request, @Nullable final JSONObject data) {
request.setAugmenter((b) -> IdHelper.addHeader(b, "Authorization", BuildConfig.ID_F));
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) 2017-2021 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.service.shift.provider.exolix;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.util.AmountHelper;
import com.m2049r.xmrwallet.util.Helper;
import org.json.JSONException;
import org.json.JSONObject;
import lombok.Getter;
@Getter
class QueryOrderParametersImpl implements QueryOrderParameters {
private final double lowerLimit;
private final double price;
private final double upperLimit;
QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException {
price = jsonObject.getDouble("rate");
lowerLimit = jsonObject.getDouble("minAmount"); // XMR
upperLimit = jsonObject.getDouble("maxAmount"); // XMR
}
public static void call(@NonNull final ShiftApiCall api, CryptoAmount btcAmount,
@NonNull final ShiftCallback<QueryOrderParameters> callback) {
if (btcAmount.getAmount() == 0) { // just checking rate without real amount
btcAmount = new CryptoAmount(Crypto.withSymbol(Helper.BASE_CRYPTO), 1); // might as well check for 1 XMR
}
final CryptoAmount cryptoAmount = btcAmount;
final StringBuilder params = new StringBuilder();
if (btcAmount.getCrypto() == Crypto.XMR) { // we are sending XMR, so float
params.append("rateType=float");
params.append("&amount=").append(AmountHelper.format(cryptoAmount.getAmount()));
params.append("&coinFrom=XMR");
} else { // we are receiving non-XMR, i.e. paying something, so fixed
params.append("rateType=fixed");
params.append("&withdrawalAmount=").append(AmountHelper.format(cryptoAmount.getAmount()));
params.append("&coinFrom=XMR");
}
params.append("&coinTo=").append(ShiftService.ASSET.getSymbol());
params.append("&networkTo=").append(ShiftService.ASSET.getNetwork());
api.get("rate", params.toString(), new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderParametersImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex, JSONObject json) {
if ((json != null) && json.has("minAmount")) {
try {
final double lowerLimit = json.getDouble((cryptoAmount.getCrypto() == Crypto.XMR) ? "minAmount" : "withdrawMin");
call(api, cryptoAmount.newWithAmount(1.5 * lowerLimit), callback); // try again with 150% of minimum
} catch (JSONException jex) {
callback.onError(jex);
}
} else {
callback.onError(ex);
}
}
});
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright (c) 2017-2021 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.service.shift.provider.exolix;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.util.DateHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
class QueryOrderStatusImpl extends QueryOrderStatus {
private QueryOrderStatusImpl(String orderId, Status status, String btcCurrency, double btcAmount, String btcAddress, double xmrAmount, String xmrAddress, Date createdAt, Date expiresAt) {
super(orderId, status, btcCurrency, btcAmount, btcAddress, xmrAmount, xmrAddress, createdAt, expiresAt);
}
static private Status getStatus(String status) {
switch (status) {
case "wait":
return Status.WAITING;
case "confirmation":
case "confirmed":
return Status.PENDING;
case "exchanging":
case "sending":
return Status.SETTLING;
case "success":
return Status.SETTLED;
case "overdue":
return Status.EXPIRED;
case "refunded":
default:
return Status.UNDEFINED;
}
}
static private QueryOrderStatusImpl of(final JSONObject jsonObject) throws JSONException {
final JSONObject coinFrom = jsonObject.getJSONObject("coinFrom");
final JSONObject coinTo = jsonObject.getJSONObject("coinTo");
// sanity checks
final String depositMethod = coinFrom.getString("coinCode");
final String settleMethod = coinTo.getString("coinCode");
if (!"xmr".equalsIgnoreCase(depositMethod)
|| !ShiftService.ASSET.getSymbol().equalsIgnoreCase(settleMethod))
throw new IllegalStateException();
final double btcAmount = jsonObject.getDouble("amountTo");
final String btcAddress = jsonObject.getString("withdrawalAddress");
final double xmrAmount = jsonObject.getDouble("amount");
final String xmrAddress = jsonObject.getString("depositAddress");
final String orderId = jsonObject.getString("id");
Date createdAt;
Date expiresAt;
try {
final String created = jsonObject.getString("createdAt");
createdAt = DateHelper.parse(created);
expiresAt = new Date(createdAt.getTime() + 300000);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
final String status = jsonObject.getString("status");
return new QueryOrderStatusImpl(
orderId,
getStatus(status),
settleMethod,
btcAmount,
btcAddress,
xmrAmount,
xmrAddress,
createdAt,
expiresAt
);
}
public static void call(@NonNull final ShiftApiCall api, @NonNull final String orderId,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
api.get("transactions/" + orderId, null, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(QueryOrderStatusImpl.of(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex, JSONObject json) {
callback.onError(ex);
}
});
}
}

View File

@ -1,65 +0,0 @@
/*
* Copyright (c) 2017-2021 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.service.shift.sideshift.api;
import java.util.Date;
public interface QueryOrderStatus {
enum State {
WAITING, // Waiting for mempool
PENDING, // Detected (waiting for confirmations)
SETTLING, // Settlement in progress
SETTLED, // Settlement completed
// no refunding in monerujo so theese are ignored:
// REFUND, // Queued for refund
// REFUNDING, // Refund in progress
// REFUNDED // Refund completed
UNDEFINED
}
boolean isCreated();
boolean isTerminal();
boolean isWaiting();
boolean isPending();
boolean isSent();
boolean isPaid();
boolean isError();
QueryOrderStatus.State getState();
String getOrderId();
Date getCreatedAt();
Date getExpiresAt();
double getBtcAmount();
String getBtcAddress();
double getXmrAmount();
String getXmrAddress();
double getPrice();
}

View File

@ -1,121 +0,0 @@
/*
* Copyright (c) 2017-2021 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.service.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.util.DateHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
import lombok.Getter;
class CreateOrderImpl implements CreateOrder {
@Getter
private final String btcCurrency;
@Getter
private final double btcAmount;
@Getter
private final String btcAddress;
@Getter
private final String quoteId;
@Getter
private final String orderId;
@Getter
private final double xmrAmount;
@Getter
private final String xmrAddress;
@Getter
private final Date createdAt;
@Getter
private final Date expiresAt;
CreateOrderImpl(final JSONObject jsonObject) throws JSONException {
// sanity checks
final String depositMethod = jsonObject.getString("depositMethodId");
final String settleMethod = jsonObject.getString("settleMethodId");
if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod))
throw new IllegalStateException();
btcCurrency = settleMethod.toUpperCase();
btcAmount = jsonObject.getDouble("settleAmount");
JSONObject settleAddress = jsonObject.getJSONObject("settleAddress");
btcAddress = settleAddress.getString("address");
xmrAmount = jsonObject.getDouble("depositAmount");
JSONObject depositAddress = jsonObject.getJSONObject("depositAddress");
xmrAddress = depositAddress.getString("address");
quoteId = jsonObject.getString("quoteId");
orderId = jsonObject.getString("orderId");
try {
final String created = jsonObject.getString("createdAtISO");
createdAt = DateHelper.parse(created);
final String expires = jsonObject.getString("expiresAtISO");
expiresAt = DateHelper.parse(expires);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
}
public static void call(@NonNull final ShiftApiCall api, final String quoteId, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
try {
final JSONObject request = createRequest(quoteId, btcAddress);
api.call("orders", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new CreateOrderImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
static JSONObject createRequest(final String quoteId, final String address) throws JSONException {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("type", "fixed");
jsonObject.put("quoteId", quoteId);
jsonObject.put("settleAddress", address);
if (!BuildConfig.ID_A.isEmpty() && !"null".equals(BuildConfig.ID_A)) {
jsonObject.put("affiliateId", BuildConfig.ID_A);
}
return jsonObject;
}
}

View File

@ -1,72 +0,0 @@
/*
* Copyright (c) 2017-2021 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.service.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.util.ServiceHelper;
import org.json.JSONException;
import org.json.JSONObject;
class QueryOrderParametersImpl implements QueryOrderParameters {
private double lowerLimit;
private double price;
private double upperLimit;
public double getLowerLimit() {
return lowerLimit;
}
public double getPrice() {
return price;
}
public double getUpperLimit() {
return upperLimit;
}
QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException {
lowerLimit = jsonObject.getDouble("min");
price = jsonObject.getDouble("rate");
upperLimit = jsonObject.getDouble("max");
}
public static void call(@NonNull final ShiftApiCall api,
@NonNull final ShiftCallback<QueryOrderParameters> callback) {
api.call("pairs/xmr/" + ServiceHelper.ASSET, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderParametersImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
}
}

View File

@ -1,145 +0,0 @@
/*
* Copyright (c) 2017-2021 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.service.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.util.DateHelper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
import lombok.Getter;
class QueryOrderStatusImpl implements QueryOrderStatus {
@Getter
private QueryOrderStatus.State state;
@Getter
private final String orderId;
@Getter
private final Date createdAt;
@Getter
private final Date expiresAt;
@Getter
private final double btcAmount;
@Getter
private final String btcAddress;
@Getter
private final double xmrAmount;
@Getter
private final String xmrAddress;
public boolean isCreated() {
return true;
}
public boolean isTerminal() {
return (state.equals(State.SETTLED) || isError());
}
public boolean isError() {
return state.equals(State.UNDEFINED);
}
public boolean isWaiting() {
return state.equals(State.WAITING);
}
public boolean isPending() {
return state.equals(State.PENDING);
}
public boolean isSent() {
return state.equals(State.SETTLING);
}
public boolean isPaid() {
return state.equals(State.SETTLED);
}
public double getPrice() {
return btcAmount / xmrAmount;
}
QueryOrderStatusImpl(final JSONObject jsonObject) throws JSONException {
try {
String created = jsonObject.getString("createdAtISO");
createdAt = DateHelper.parse(created);
String expires = jsonObject.getString("expiresAtISO");
expiresAt = DateHelper.parse(expires);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
orderId = jsonObject.getString("orderId");
btcAmount = jsonObject.getDouble("settleAmount");
JSONObject settleAddress = jsonObject.getJSONObject("settleAddress");
btcAddress = settleAddress.getString("address");
xmrAmount = jsonObject.getDouble("depositAmount");
JSONObject depositAddress = jsonObject.getJSONObject("depositAddress");
xmrAddress = settleAddress.getString("address");
JSONArray deposits = jsonObject.getJSONArray("deposits");
// we only create one deposit, so die if there are more than one:
if (deposits.length() > 1)
throw new IllegalStateException("more than one deposits");
state = State.UNDEFINED;
if (deposits.length() == 0) {
state = State.WAITING;
} else if (deposits.length() == 1) {
// sanity check
if (!orderId.equals(deposits.getJSONObject(0).getString("orderId")))
throw new IllegalStateException("deposit has different order id!");
String stateName = deposits.getJSONObject(0).getString("status");
try {
state = State.valueOf(stateName.toUpperCase());
} catch (IllegalArgumentException ex) {
state = State.UNDEFINED;
}
}
}
public static void call(@NonNull final ShiftApiCall api, @NonNull final String orderId,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
api.call("orders/" + orderId, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderStatusImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
}
}

View File

@ -1,126 +0,0 @@
/*
* Copyright (c) 2017-2021 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.service.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
import com.m2049r.xmrwallet.util.DateHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;
import lombok.Getter;
class RequestQuoteImpl implements RequestQuote {
@Getter
private final double btcAmount;
@Getter
private final String id;
@Getter
private final Date createdAt;
@Getter
private final Date expiresAt;
@Getter
private final double xmrAmount;
@Getter
private final double price;
// TODO do something with errors - they always seem to send us 500
RequestQuoteImpl(final JSONObject jsonObject) throws JSONException {
// sanity checks
final String depositMethod = jsonObject.getString("depositMethod");
final String settleMethod = jsonObject.getString("settleMethod");
if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod))
throw new IllegalStateException();
btcAmount = jsonObject.getDouble("settleAmount");
id = jsonObject.getString("id");
try {
final String created = jsonObject.getString("createdAt");
createdAt = DateHelper.parse(created);
final String expires = jsonObject.getString("expiresAt");
expiresAt = DateHelper.parse(expires);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
xmrAmount = jsonObject.getDouble("depositAmount");
price = jsonObject.getDouble("rate");
}
public static void call(@NonNull final ShiftApiCall api, final double btcAmount,
@NonNull final ShiftCallback<RequestQuote> callback) {
try {
final JSONObject request = createRequest(btcAmount);
api.call("quotes", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new RequestQuoteImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
/**
* Create JSON request object
*
* @param btcAmount how much XMR to shift to BTC
*/
static JSONObject createRequest(final double btcAmount) throws JSONException {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("depositMethod", "xmr");
jsonObject.put("settleMethod", ServiceHelper.ASSET);
// #sideshift is silly and likes numbers as strings
String amount = AmountFormatter.format(btcAmount);
jsonObject.put("settleAmount", amount);
return jsonObject;
}
static final DecimalFormat AmountFormatter;
static {
AmountFormatter = new DecimalFormat();
AmountFormatter.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
AmountFormatter.setMinimumIntegerDigits(1);
AmountFormatter.setMaximumFractionDigits(12);
AmountFormatter.setGroupingUsed(false);
}
}

View File

@ -1,122 +0,0 @@
/*
* Copyright (c) 2017-2021 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.service.shift.sideshift.network;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.Response;
import timber.log.Timber;
public class SideShiftApiImpl implements SideShiftApi, ShiftApiCall {
private final HttpUrl baseUrl;
public SideShiftApiImpl(final HttpUrl baseUrl) {
this.baseUrl = baseUrl;
}
@Override
public void queryOrderParameters(@NonNull final ShiftCallback<QueryOrderParameters> callback) {
QueryOrderParametersImpl.call(this, callback);
}
@Override
public void requestQuote(final double btcAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
RequestQuoteImpl.call(this, btcAmount, callback);
}
@Override
public void createOrder(final String quoteId, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
CreateOrderImpl.call(this, quoteId, btcAddress, callback);
}
@Override
public void queryOrderStatus(@NonNull final String uuid,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
QueryOrderStatusImpl.call(this, uuid, callback);
}
@Override
public Uri getQueryOrderUri(String orderId) {
return Uri.parse("https://sideshift.ai/orders/" + orderId);
}
@Override
public void call(@NonNull final String path, @NonNull final NetworkCallback callback) {
call(path, null, callback);
}
@Override
public void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback) {
final HttpUrl url = baseUrl.newBuilder()
.addPathSegments(path)
.build();
NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url, request);
httpRequest.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(final Call call, final IOException ex) {
callback.onError(ex);
}
@Override
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
Timber.d("onResponse code=%d", response.code());
if (response.isSuccessful()) {
try {
final JSONObject json = new JSONObject(response.body().string());
callback.onSuccess(json);
} catch (JSONException ex) {
callback.onError(ex);
}
} else {
try {
final JSONObject json = new JSONObject(response.body().string());
Timber.d(json.toString(2));
final ShiftError error = new ShiftError(json);
Timber.w("%s says %d/%s", CreateOrder.TAG, response.code(), error.toString());
callback.onError(new ShiftException(response.code(), error));
} catch (JSONException ex) {
callback.onError(new ShiftException(response.code()));
}
}
}
});
}
}

View File

@ -0,0 +1,28 @@
package com.m2049r.xmrwallet.util;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
public class AmountHelper {
static final DecimalFormat AmountFormatter_12;
static {
AmountFormatter_12 = new DecimalFormat();
AmountFormatter_12.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
AmountFormatter_12.setMinimumIntegerDigits(1);
AmountFormatter_12.setMaximumFractionDigits(12);
AmountFormatter_12.setGroupingUsed(false);
}
public static String format(double amount) {
return AmountFormatter_12.format(amount);
}
public static String format_6(double amount) {
int n = (int) Math.ceil(Math.log10(amount));
int d = Math.max(2, 6 - n);
final String fmt = "%,." + d + "f";
return String.format(Locale.getDefault(), fmt, amount);
}
}

View File

@ -78,12 +78,13 @@ import timber.log.Timber;
public class Helper {
static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass";
static public final String BASE_CRYPTO = Crypto.XMR.getSymbol();
static public final Crypto BASE_CRYPTO_CRYPTO = Crypto.XMR;
static public final String BASE_CRYPTO = BASE_CRYPTO_CRYPTO.getSymbol();
static public final int XMR_DECIMALS = 12;
static public final long ONE_XMR = Math.round(Math.pow(10, Helper.XMR_DECIMALS));
static public final boolean SHOW_EXCHANGERATES = true;
static public boolean ALLOW_SHIFT = false;
static public boolean ALLOW_SHIFT = true;
static private final String WALLET_DIR = "wallets";
static private final String MONERO_DIR = "monero";

View File

@ -0,0 +1,34 @@
package com.m2049r.xmrwallet.util;
import com.m2049r.xmrwallet.BuildConfig;
import org.json.JSONException;
import org.json.JSONObject;
import okhttp3.Request;
public class IdHelper {
static public String idOrNot(String id) {
return isId(id) ? id : "";
}
static public boolean isId(String id) {
return (id != null) && !id.isEmpty() && !"null".equals(id);
}
static public String asParameter(String name, String id) {
return isId(id) ? (name + "=" + id) : "";
}
static public void jsonPut(JSONObject jsonObject, String name, String id) throws JSONException {
if (isId(id)) {
jsonObject.put(name, id);
}
}
static public void addHeader(Request.Builder builder, String name, String id) {
if (isId(id)) {
builder.addHeader(name, id);
}
}
}

View File

@ -45,6 +45,7 @@ import info.guardianproject.netcipher.proxy.MyOrbotHelper;
import info.guardianproject.netcipher.proxy.SignatureUtils;
import info.guardianproject.netcipher.proxy.StatusCallback;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import okhttp3.Call;
import okhttp3.Callback;
@ -58,11 +59,11 @@ import timber.log.Timber;
@RequiredArgsConstructor
public class NetCipherHelper implements StatusCallback {
public static final String USER_AGENT = "Monerujo/1.0";
public static final int HTTP_TIMEOUT_CONNECT = 1000; //ms
public static final int HTTP_TIMEOUT_READ = 2000; //ms
public static final int HTTP_TIMEOUT_WRITE = 1000; //ms
public static final int TOR_TIMEOUT_CONNECT = 5000; //ms
public static final int TOR_TIMEOUT = 2000; //ms
public static final int HTTP_TIMEOUT_CONNECT = 2500; //ms
public static final int HTTP_TIMEOUT_READ = 5000; //ms
public static final int HTTP_TIMEOUT_WRITE = 2500; //ms
public static final int TOR_TIMEOUT_CONNECT = 10000; //ms
public static final int TOR_TIMEOUT = 5000; //ms
public interface OnStatusChangedListener {
void connected();
@ -112,7 +113,6 @@ public class NetCipherHelper implements StatusCallback {
.withSocksProxy()
.applyTo(okBuilder, statusIntent)
.build();
Helper.ALLOW_SHIFT = false; // no shifting with Tor
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
@ -125,7 +125,6 @@ public class NetCipherHelper implements StatusCallback {
.writeTimeout(HTTP_TIMEOUT_WRITE, TimeUnit.MILLISECONDS)
.readTimeout(HTTP_TIMEOUT_READ, TimeUnit.MILLISECONDS)
.build();
Helper.ALLOW_SHIFT = true;
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
@ -295,19 +294,36 @@ public class NetCipherHelper implements StatusCallback {
@ToString
static public class Request {
final HttpUrl url;
final String json;
final JSONObject data;
final String username;
final String password;
@Setter
RequestAugmenter augmenter;
public Request(final HttpUrl url, final String json, final String username, final String password) {
this.url = url;
this.json = json;
this.username = username;
this.password = password;
final Method method;
public enum Method {
GET, POST;
}
public Request(final HttpUrl url, final JSONObject json) {
this(url, json == null ? null : json.toString(), null, null);
public interface RequestAugmenter {
void augment(okhttp3.Request.Builder builder);
}
public Request(final HttpUrl url, final JSONObject data, final String username, final String password) {
this.url = url;
this.data = data;
this.username = username;
this.password = password;
if (data == null) {
method = Method.GET;
} else {
method = Method.POST;
}
}
public Request(final HttpUrl url, final JSONObject data) {
this(url, data, null, null);
}
public Request(final HttpUrl url) {
@ -346,11 +362,16 @@ public class NetCipherHelper implements StatusCallback {
final okhttp3.Request.Builder builder =
new okhttp3.Request.Builder()
.url(url)
.header("User-Agent", USER_AGENT);
if (json != null) {
builder.post(RequestBody.create(json, MediaType.parse("application/json")));
} else {
builder.get();
.header("User-Agent", USER_AGENT)
.header("Accept", "application/json");
if (augmenter != null) augmenter.augment(builder);
switch (method) {
case GET:
builder.get();
break;
case POST:
builder.post(RequestBody.create(data.toString(), MediaType.parse("application/json")));
break;
}
return builder.build();
}

View File

@ -143,8 +143,8 @@ public class OpenAliasHelper {
for (String txt : txts) {
BarcodeData bc = BarcodeData.parseOpenAlias(txt, dnssec);
if (bc != null) {
if (!dataMap.containsKey(bc.asset)) {
dataMap.put(bc.asset, bc);
if (!dataMap.containsKey(bc.getAsset())) {
dataMap.put(bc.getAsset(), bc);
}
}
}

View File

@ -7,16 +7,6 @@ import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
import okhttp3.HttpUrl;
public class ServiceHelper {
public static String ASSET = null;
static public HttpUrl getXmrToBaseUrl() {
if ((WalletManager.getInstance() == null)
|| (WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet)) {
throw new IllegalStateException("Only mainnet not supported");
} else {
return HttpUrl.parse("https://sideshift.ai/api/v1/");
}
}
static public ExchangeApi getExchangeApi() {
return new com.m2049r.xmrwallet.service.exchange.krakenFiat.ExchangeApiImpl();

View File

@ -1,51 +0,0 @@
package com.m2049r.xmrwallet.util.validator;
import lombok.Getter;
public enum BitcoinAddressType {
BTC(Type.BTC, Type.BTC_BECH32_PREFIX),
LTC(Type.LTC, Type.LTC_BECH32_PREFIX),
DASH(Type.DASH, null),
DOGE(Type.DOGE, null);
@Getter
private final byte[] production;
@Getter
private final byte[] testnet;
@Getter
private final String productionBech32Prefix;
@Getter
private final String testnetBech32Prefix;
public boolean hasBech32() {
return productionBech32Prefix != null;
}
public String getBech32Prefix(boolean testnet) {
return testnet ? testnetBech32Prefix : productionBech32Prefix;
}
BitcoinAddressType(byte[][] types, String[] bech32Prefix) {
production = types[0];
testnet = types[1];
if (bech32Prefix != null) {
productionBech32Prefix = bech32Prefix[0];
testnetBech32Prefix = bech32Prefix[1];
} else {
productionBech32Prefix = null;
testnetBech32Prefix = null;
}
}
// Java is silly and doesn't allow array initializers in the construction
private static class Type {
private static final byte[][] BTC = {{0x00, 0x05}, {0x6f, (byte) 0xc4}};
private static final String[] BTC_BECH32_PREFIX = {"bc", "tb"};
private static final byte[][] LTC = {{0x30, 0x05, 0x32}, {0x6f, (byte) 0xc4, 0x3a}};
private static final String[] LTC_BECH32_PREFIX = {"ltc", "tltc"};
private static final byte[][] DASH = {{0x4c, 0x10}, {(byte) 0x8c, 0x13}};
private static final byte[][] DOGE = {{0x1e, 0x16}, {0x71, (byte) 0xc4}};
}
}

View File

@ -1,220 +0,0 @@
/*
* Copyright (c) 2017 m2049r er 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.validator;
// mostly based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.WalletManager;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class BitcoinAddressValidator {
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public static Crypto validate(String address) {
for (BitcoinAddressType type : BitcoinAddressType.values()) {
if (validate(address, type))
return Crypto.valueOf(type.name());
}
return null;
}
// just for tests
public static boolean validateBTC(String addrress, boolean testnet) {
return validate(addrress, BitcoinAddressType.BTC, testnet);
}
public static boolean validate(String addrress, BitcoinAddressType type, boolean testnet) {
if (validate(addrress, testnet ? type.getTestnet() : type.getProduction()))
return true;
if (type.hasBech32())
return validateBech32Segwit(addrress, type, testnet);
else
return false;
}
public static boolean validate(String addrress, BitcoinAddressType type) {
final boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet;
return validate(addrress, type, testnet);
}
public static boolean validate(String addrress, byte[] addressTypes) {
if (addrress.length() < 26 || addrress.length() > 35)
return false;
byte[] decoded = decodeBase58To25Bytes(addrress);
if (decoded == null)
return false;
int v = decoded[0] & 0xFF;
boolean nok = true;
for (byte b : addressTypes) {
nok = nok && (v != (b & 0xFF));
}
if (nok) return false;
byte[] hash1 = sha256(Arrays.copyOfRange(decoded, 0, 21));
byte[] hash2 = sha256(hash1);
return Arrays.equals(Arrays.copyOfRange(hash2, 0, 4), Arrays.copyOfRange(decoded, 21, 25));
}
private static byte[] decodeBase58To25Bytes(String input) {
BigInteger num = BigInteger.ZERO;
for (char t : input.toCharArray()) {
int p = ALPHABET.indexOf(t);
if (p == -1)
return null;
num = num.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(p));
}
byte[] result = new byte[25];
byte[] numBytes = num.toByteArray();
if (num.bitLength() > 200) return null;
if (num.bitLength() == 200) {
System.arraycopy(numBytes, 1, result, 0, 25);
} else {
System.arraycopy(numBytes, 0, result, result.length - numBytes.length, numBytes.length);
}
return result;
}
private static byte[] sha256(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(data);
return md.digest();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
//
// validate Bech32 segwit
// see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki for spec
//
private static final String DATA_CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
public static boolean validateBech32Segwit(String bech32, BitcoinAddressType type, boolean testnet) {
if (!bech32.equals(bech32.toLowerCase()) && !bech32.equals(bech32.toUpperCase())) {
return false; // mixing upper and lower case not allowed
}
bech32 = bech32.toLowerCase();
if (!bech32.startsWith(type.getBech32Prefix(testnet))) return false;
final int hrpLength = type.getBech32Prefix(testnet).length();
if ((bech32.length() < (12 + hrpLength)) || (bech32.length() > (72 + hrpLength)))
return false;
int mod = (bech32.length() - hrpLength) % 8;
if ((mod == 6) || (mod == 1) || (mod == 3)) return false;
int sep = -1;
final byte[] bytes = bech32.getBytes(StandardCharsets.US_ASCII);
for (int i = 0; i < bytes.length; i++) {
if ((bytes[i] < 33) || (bytes[i] > 126)) {
return false;
}
if (bytes[i] == 49) sep = i; // 49 := '1' in ASCII
}
if (sep != hrpLength) return false;
if (sep > bytes.length - 7) {
return false; // min 6 bytes data
}
if (bytes.length < 8) { // hrp{min}=1 + sep=1 + data{min}=6 := 8
return false; // too short
}
if (bytes.length > 90) {
return false; // too long
}
final byte[] hrp = Arrays.copyOfRange(bytes, 0, sep);
final byte[] data = Arrays.copyOfRange(bytes, sep + 1, bytes.length);
for (int i = 0; i < data.length; i++) {
int b = DATA_CHARS.indexOf(data[i]);
if (b < 0) return false; // invalid character
data[i] = (byte) b;
}
if (!validateBech32Data(data)) return false;
return verifyChecksum(hrp, data);
}
private static int polymod(byte[] values) {
final int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
int chk = 1;
for (byte v : values) {
byte b = (byte) (chk >> 25);
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (int i = 0; i < 5; i++) {
chk ^= ((b >> i) & 1) == 1 ? GEN[i] : 0;
}
}
return chk;
}
private static byte[] hrpExpand(byte[] hrp) {
final byte[] expanded = new byte[(2 * hrp.length) + 1];
int i = 0;
for (byte b : hrp) {
expanded[i++] = (byte) (b >> 5);
}
expanded[i++] = 0;
for (byte b : hrp) {
expanded[i++] = (byte) (b & 0x1f);
}
return expanded;
}
private static boolean verifyChecksum(byte[] hrp, byte[] data) {
final byte[] hrpExpanded = hrpExpand(hrp);
final byte[] values = new byte[hrpExpanded.length + data.length];
System.arraycopy(hrpExpanded, 0, values, 0, hrpExpanded.length);
System.arraycopy(data, 0, values, hrpExpanded.length, data.length);
return (polymod(values) == 1);
}
private static boolean validateBech32Data(final byte[] data) {
if ((data[0] < 0) || (data[0] > 16)) return false; // witness version
final int programLength = data.length - 1 - 6; // 1-byte version at beginning & 6-byte checksum at end
// since we are coming from our own decoder, we don't need to verify data is 5-bit bytes
final int convertedSize = programLength * 5 / 8;
final int remainderSize = programLength * 5 % 8;
if ((convertedSize < 2) || (convertedSize > 40)) return false;
if ((data[0] == 0) && (convertedSize != 20) && (convertedSize != 32)) return false;
if (remainderSize >= 5) return false;
// ignore checksum at end and get last byte of program
if ((data[data.length - 1 - 6] & ((1 << remainderSize) - 1)) != 0) return false;
return true;
}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright (c) 2017 m2049r er 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.validator;
// mostly based on https://github.com/ognus/wallet-address-validator/blob/master/src/ethereum_validator.js
import com.theromus.sha.Keccak;
import com.theromus.sha.Parameters;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
public class EthAddressValidator {
static private final Pattern ETH_ADDRESS = Pattern.compile("^0x[0-9a-fA-F]{40}$");
static private final Pattern ETH_ALLLOWER = Pattern.compile("^0x[0-9a-f]{40}$");
static private final Pattern ETH_ALLUPPER = Pattern.compile("^0x[0-9A-F]{40}$");
public static boolean validate(String address) {
// Check if it has the basic requirements of an address
if (!ETH_ADDRESS.matcher(address).matches())
return false;
// If it's all small caps or all all caps, return true
if (ETH_ALLLOWER.matcher(address).matches() || ETH_ALLUPPER.matcher(address).matches()) {
return true;
}
// Otherwise check each case
return validateChecksum(address);
}
private static boolean validateChecksum(String address) {
// Check each case
address = address.substring(2); // strip 0x
Keccak keccak = new Keccak();
final byte[] addressHash = keccak.getHash(
address.toLowerCase().getBytes(StandardCharsets.US_ASCII),
Parameters.KECCAK_256);
for (int i = 0; i < 40; i++) {
boolean upper = (addressHash[i / 2] & ((i % 2) == 0 ? 128 : 8)) != 0;
char c = address.charAt(i);
if (Character.isAlphabetic(c)) {
if (Character.isUpperCase(c) && !upper) return false;
if (Character.isLowerCase(c) && upper) return false;
}
}
return true;
}
}

View File

@ -39,7 +39,8 @@ public class CTextInputLayout extends TextInputLayout {
@Override
public int getBaseline() {
EditText editText = getEditText();
final EditText editText = getEditText();
assert editText != null;
return editText.getBaseline() - (getMeasuredHeight() - editText.getMeasuredHeight());
}
}

View File

@ -25,8 +25,11 @@ import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import lombok.Getter;
import timber.log.Timber;
public class DotBar extends View {
@ -37,7 +40,9 @@ public class DotBar extends View {
final private float dotSize;
private float dotSpacing;
@Getter
final private int numDots;
@Getter
private int activeDot;
final private Paint paint;
@ -45,17 +50,13 @@ public class DotBar extends View {
public DotBar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DotBar, 0, 0);
try {
try (TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DotBar, 0, 0)) {
inactiveColor = ta.getInt(R.styleable.DotBar_inactiveColor, 0);
activeColor = ta.getInt(R.styleable.DotBar_activeColor, 0);
dotSize = ta.getDimensionPixelSize(R.styleable.DotBar_dotSize, 8);
numDots = ta.getInt(R.styleable.DotBar_numberDots, 5);
activeDot = ta.getInt(R.styleable.DotBar_activeDot, 0);
} finally {
ta.recycle();
}
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
}
@ -105,14 +106,14 @@ public class DotBar extends View {
}
@Override
protected void onDraw(Canvas canvas) {
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
// Centering the dots in the middle of the canvas
float singleDotSize = dotSpacing + dotSize;
float combinedDotSize = singleDotSize * numDots - dotSpacing;
int startingX = (int) ((canvas.getWidth() - combinedDotSize) / 2);
int startingY = (int) ((canvas.getHeight() - dotSize) / 2);
int startingX = (int) ((getWidth() - combinedDotSize) / 2);
int startingY = (int) ((getHeight() - dotSize) / 2);
for (int i = 0; i < numDots; i++) {
int x = (int) (startingX + i * singleDotSize);
@ -145,12 +146,4 @@ public class DotBar extends View {
invalidate();
}
}
public int getActiveDot() {
return activeDot;
}
public int getNumDots() {
return numDots;
}
}

View File

@ -400,7 +400,6 @@ public class ExchangeEditText extends LinearLayout {
exchangeRate.getQuoteCurrency(), sCurrencyB.getSelectedItem());
return;
}
exchangeRateCache = exchangeRate;
if (prepareExchange()) {
exchange(exchangeRate.getRate());

View File

@ -28,6 +28,8 @@ import android.widget.Spinner;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
import com.m2049r.xmrwallet.util.Helper;
@ -35,9 +37,18 @@ import com.m2049r.xmrwallet.util.Helper;
import java.util.ArrayList;
import java.util.List;
import lombok.Setter;
import timber.log.Timber;
public class ExchangeOtherEditText extends ExchangeEditText {
public interface Listener {
void onExchangeRequested();
}
@Setter
private Listener listener = null;
/*
all exchanges are done through XMR
baseCurrency is the native currency
@ -60,13 +71,10 @@ public class ExchangeOtherEditText extends ExchangeEditText {
}
private void setBaseCurrency(Context context, AttributeSet attrs) {
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExchangeEditText, 0, 0);
try {
try (TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExchangeEditText, 0, 0)) {
baseCurrency = ta.getString(R.styleable.ExchangeEditText_baseSymbol);
if (baseCurrency == null)
throw new IllegalArgumentException("base currency must be set");
} finally {
ta.recycle();
}
}
@ -133,7 +141,7 @@ public class ExchangeOtherEditText extends ExchangeEditText {
// first deal with XMR/baseCurrency & baseCurrency/XMR
if (currencyA.equals(Helper.BASE_CRYPTO) && (currencyB.equals(baseCurrency))) {
localExchange(currencyA, currencyB, 1.0d / exchangeRate);
localExchange(currencyA, currencyB, (exchangeRate > 0) ? (1.0d / exchangeRate) : 0);
return;
}
if (currencyA.equals(baseCurrency) && (currencyB.equals(Helper.BASE_CRYPTO))) {
@ -157,32 +165,29 @@ public class ExchangeOtherEditText extends ExchangeEditText {
@Override
public void onSuccess(final ExchangeRate exchangeRate) {
if (isAttachedToWindow())
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ExchangeRate xchange = new ExchangeRate() {
@Override
public String getServiceName() {
return exchangeRate.getServiceName() + "+" + baseCurrency;
}
new Handler(Looper.getMainLooper()).post(() -> {
ExchangeRate xchange = new ExchangeRate() {
@Override
public String getServiceName() {
return exchangeRate.getServiceName() + "+" + baseCurrency;
}
@Override
public String getBaseCurrency() {
return baseIsBaseCrypto ? baseCurrency : base;
}
@Override
public String getBaseCurrency() {
return baseIsBaseCrypto ? baseCurrency : base;
}
@Override
public String getQuoteCurrency() {
return baseIsBaseCrypto ? quote : baseCurrency;
}
@Override
public String getQuoteCurrency() {
return baseIsBaseCrypto ? quote : baseCurrency;
}
@Override
public double getRate() {
return exchangeRate.getRate() * factor;
}
};
exchange(xchange);
}
@Override
public double getRate() {
return exchangeRate.getRate() * factor;
}
};
exchange(xchange);
});
}
@ -193,4 +198,35 @@ public class ExchangeOtherEditText extends ExchangeEditText {
}
});
}
@Override
public void doExchange() {
super.doExchange();
if (listener != null)
listener.onExchangeRequested();
}
private double getCleanAmount(String enteredAmount) {
try {
return Double.parseDouble(enteredAmount);
} catch (NumberFormatException ex) {
return 0;
}
}
public CryptoAmount getPrimaryAmount() {
// we can send xmr (=float) or baseCurrency (=fixed)
if (getCurrencyA() == 0) { // baseCurrency
// send it
return new CryptoAmount(Crypto.withSymbol(baseCurrency), getCleanAmount(etAmountA.getEditText().getText().toString()));
} else if (getCurrencyA() == 1) { // XMR
// send XMR
return new CryptoAmount(Helper.BASE_CRYPTO_CRYPTO, getCleanAmount(etAmountA.getEditText().getText().toString()));
} else if (getCurrencyB() == 0) { // fiat is on A (currencyB must be baseCurrency)
// send baseCurrency shown on B
return new CryptoAmount(Crypto.withSymbol(baseCurrency), getCleanAmount(tvAmountB.getText().toString()));
} else {
throw new IllegalStateException("B is not base");
}
}
}

View File

@ -51,6 +51,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import lombok.Setter;
import timber.log.Timber;
public class ExchangeView extends LinearLayout {
@ -441,29 +442,20 @@ public class ExchangeView extends LinearLayout {
void onNewAmount(String xmr);
}
@Setter
OnNewAmountListener onNewAmountListener;
public void setOnNewAmountListener(OnNewAmountListener listener) {
onNewAmountListener = listener;
}
public interface OnAmountInvalidatedListener {
void onAmountInvalidated();
}
@Setter
OnAmountInvalidatedListener onAmountInvalidatedListener;
public void setOnAmountInvalidatedListener(OnAmountInvalidatedListener listener) {
onAmountInvalidatedListener = listener;
}
public interface OnFailedExchangeListener {
void onFailedExchange();
}
@Setter
OnFailedExchangeListener onFailedExchangeListener;
public void setOnFailedExchangeListener(OnFailedExchangeListener listener) {
onFailedExchangeListener = listener;
}
}

View File

@ -23,6 +23,8 @@ import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.R;
public class SendProgressView extends LinearLayout {
@ -75,7 +77,7 @@ public class SendProgressView extends LinearLayout {
llMessage.setVisibility(INVISIBLE);
}
public void showMessage(String code, String message, String solution) {
public void showMessage(String code, String message, @Nullable String solution) {
tvCode.setText(code);
tvMessage.setText(message);
tvSolution.setText(solution);

View File

@ -30,6 +30,7 @@ import android.widget.TextView;
import com.google.android.material.appbar.MaterialToolbar;
import com.m2049r.xmrwallet.R;
import lombok.Setter;
import timber.log.Timber;
public class Toolbar extends MaterialToolbar {
@ -37,12 +38,9 @@ public class Toolbar extends MaterialToolbar {
void onButton(int type);
}
@Setter
OnButtonListener onButtonListener;
public void setOnButtonListener(OnButtonListener listener) {
onButtonListener = listener;
}
ImageView toolbarImage;
TextView toolbarTitle;
TextView toolbarSubtitle;

View File

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="?colorPrimary"
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" />
<path
android:fillColor="?android:colorBackground"
android:pathData="M19.086,8.004H11.81l-0.602,3.367 6.562,0.01c3.231,0 4.19,1.173 4.159,3.12 -0.014,0.998 -0.449,2.686 -0.633,3.23 -0.497,1.46 -1.521,3.122 -5.359,3.117l-6.378,-0.004 -0.602,3.371h7.257c2.559,0 3.649,-0.299 4.8,-0.83 2.554,-1.178 4.075,-3.701 4.686,-6.994 0.906,-4.9 -0.224,-8.387 -6.615,-8.387z" />
<path
android:fillColor="?android:colorBackground"
android:pathData="M15.807,15.798c0.237,-0.985 0.312,-1.38 0.312,-1.38H8.673c-1.904,0 -2.176,1.24 -2.357,1.99 -0.237,0.981 -0.312,1.381 -0.312,1.381h7.447c1.903,0 2.175,-1.24 2.356,-1.991z" />
</vector>

View File

@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="?colorPrimary"
android:fillType="evenOdd"
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" />
<path
android:fillColor="?android:colorBackground"
android:fillType="evenOdd"
android:pathData="M13.248,14.61h4.314v2.286h-4.314v4.818h2.721c1.077,0 1.958,-0.145 2.644,-0.437 0.686,-0.291 1.224,-0.694 1.615,-1.21a4.4,4.4 0,0 0,0.796 -1.815,11.4 11.4,0 0,0 0.21,-2.252 11.4,11.4 0,0 0,-0.21 -2.252,4.396 4.396,0 0,0 -0.796,-1.815c-0.391,-0.516 -0.93,-0.919 -1.615,-1.21 -0.686,-0.292 -1.567,-0.437 -2.644,-0.437h-2.721v4.325zM10.482,16.896L9,16.896v-2.285h1.482L10.482,8h6.549c1.21,0 2.257,0.21 3.142,0.627 0.885,0.419 1.607,0.99 2.168,1.715 0.56,0.724 0.977,1.572 1.25,2.543 0.273,0.971 0.409,2.01 0.409,3.115a11.47,11.47 0,0 1,-0.41 3.115c-0.272,0.97 -0.689,1.819 -1.25,2.543 -0.56,0.725 -1.282,1.296 -2.167,1.715 -0.885,0.418 -1.933,0.627 -3.142,0.627h-6.549v-7.104z" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="?android:colorBackground"
android:pathData="M16,16m-14.6,0a14.6,14.6 0,1 1,29.2 0a14.6,14.6 0,1 1,-29.2 0" />
<path
android:fillColor="?colorPrimary"
android:pathData="M16,0C7.16,0 0,7.16 0,16s7.16,16 16,16 16,-7.16 16,-16S24.84,0 16,0ZM5.86,11.97l3.54,-3.79s0.06,-0.04 0.09,-0.04h16.55c0.11,0 0.16,0.13 0.09,0.21l-3.54,3.79s-0.06,0.04 -0.09,0.04H5.95c-0.11,0 -0.16,-0.13 -0.09,-0.21ZM5.95,13.98h16.55s0.07,0.01 0.09,0.04l3.54,3.79c0.07,0.08 0.02,0.21 -0.09,0.21H9.49s-0.07,-0.01 -0.09,-0.04l-3.54,-3.79c-0.07,-0.08 -0.02,-0.21 0.09,-0.21ZM26.3,20.03l-3.54,3.79s-0.06,0.04 -0.09,0.04H6.11c-0.11,0 -0.16,-0.13 -0.09,-0.21l3.54,-3.79s0.06,-0.04 0.09,-0.04h16.55c0.11,0 0.16,0.13 0.09,0.21Z" />
</vector>

View File

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="?android:colorBackground"
android:pathData="M25,25m-6.5,0a6.5,6.5 0,1 1,13 0a6.5,6.5 0,1 1,-13 0" />
<path
android:fillColor="?android:colorBackground"
android:pathData="M17,25c0,-4.42 3.58,-8 8,-8 2.17,0 4.13,0.87 5.58,2.27 0.23,-1.05 0.37,-2.14 0.37,-3.27C30.94,7.72 24.23,1 15.94,1S0.94,7.72 0.94,16s6.72,15 15,15c1.16,0 2.28,-0.14 3.36,-0.39 -1.42,-1.44 -2.3,-3.42 -2.3,-5.61Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M27.29,22.61l-5.02,-0.93l3.1,2.55l1.92,-1.62z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M25.19,28.76l3.64,-4.38l-3.18,0.51l-0.46,3.87z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M24.64,28.76l0.47,-4.05l-3.27,-2.77l2.8,6.82z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M25,18c-3.87,0 -7,3.13 -7,7s3.13,7 7,7 7,-3.13 7,-7 -3.13,-7 -7,-7ZM29.64,24.27c-0.43,0.55 -4.65,5.67 -4.65,5.67 0,0 -0.24,0.31 -0.29,0.31h-0.02c-0.1,0 -0.16,-0.18 -0.19,-0.27 -0.48,-1.33 -2.94,-7.01 -3.57,-8.75v-0.14s0.05,-0.09 0.08,-0.12l0.03,-0.02s0.05,-0.03 0.08,-0.03c0.11,-0.01 5.64,1.02 6.83,1.24 0.06,0.02 0.11,0.05 0.16,0.09l0.03,0.02c0.49,0.44 1.1,1.05 1.54,1.46 0.05,0.04 0.08,0.09 0.1,0.15 0.02,0.06 0.03,0.12 0.01,0.18 -0.04,0.08 -0.09,0.15 -0.15,0.22Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M26.07,24.32l2.86,-0.52l-1.06,-0.97l-1.8,1.49z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M17.04,25.75h-3.05v-7.62c-4.42,-0.2 -7.75,-1.08 -7.75,-2.13s3.33,-1.93 7.75,-2.13v-2.38h-5.44v-3.63h14.82v3.63h-5.44v2.38h0c4.42,0.2 7.74,1.08 7.74,2.13 0,0.37 -0.43,0.72 -1.16,1.03 0.17,-0.01 0.34,-0.03 0.51,-0.03 2.61,0 4.91,1.25 6.37,3.18 0.36,-1.33 0.57,-2.73 0.57,-4.18C31.94,7.16 24.78,0 15.94,0S-0.06,7.16 -0.06,16 7.11,32 15.94,32c1.48,0 2.91,-0.22 4.28,-0.6 -1.76,-1.32 -2.97,-3.34 -3.18,-5.65Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M17.92,21.28c0.81,-1.54 2.11,-2.78 3.69,-3.52 -1.08,0.17 -2.34,0.3 -3.69,0.36v3.16Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M17.92,14.14v2.66c-0.11,0.01 -0.72,0.06 -1.94,0.06 -1.01,0 -1.74,-0.04 -2,-0.06v-2.66c-3.91,0.17 -6.83,0.85 -6.83,1.67s2.92,1.49 6.83,1.67h0c0.25,0 0.97,0.04 1.98,0.04 1.27,0 1.84,-0.03 1.95,-0.04h0s0,0 0,0c3.9,-0.17 6.81,-0.86 6.81,-1.67s-2.91,-1.49 -6.81,-1.67Z" />
</vector>

View File

@ -4,6 +4,6 @@
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="?colorError"
android:fillColor="?attr/negativeColor"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="103.75"
android:viewportHeight="103.75">
<path
android:fillColor="?colorPrimary"
android:pathData="M9.09,81.75l23.18,0l35.29,0l-29.67,-40.68l-28.8,40.68z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M94.26,22l-23.69,0l-36.06,0l30.31,41.95l29.44,-41.95z" />
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="663.62dp"
android:height="103.75dp"
android:viewportWidth="663.62"
android:viewportHeight="103.75">
<path
android:fillColor="?colorPrimary"
android:pathData="M147.76,56.9l31.78,0l-0.05,-11.22l-31.73,0l0,-19.09l31.76,0l8.36,-11.42l-53.81,0l0,73.39l46.99,0l8.3,-11.42l-41.6,0l0,-20.24z" />
<path
android:fillColor="?colorPrimary"
android:pathData="m337.03,19c-6.04,-3.25 -12.81,-4.88 -20.33,-4.88s-14.29,1.63 -20.32,4.88c-6.04,3.25 -10.78,7.76 -14.22,13.53 -3.44,5.77 -5.16,12.22 -5.16,19.35s1.72,13.58 5.16,19.35c3.44,5.77 8.18,10.28 14.22,13.53 6.04,3.25 12.81,4.88 20.32,4.88s14.29,-1.63 20.33,-4.88c6.04,-3.25 10.78,-7.74 14.22,-13.47 3.44,-5.73 5.16,-12.2 5.16,-19.4s-1.72,-13.67 -5.16,-19.4c-3.44,-5.73 -8.18,-10.22 -14.22,-13.47ZM339.24,65.14c-2.25,3.95 -5.34,7.03 -9.27,9.23 -3.93,2.2 -8.36,3.3 -13.27,3.3s-9.34,-1.1 -13.27,-3.3c-3.93,-2.2 -7.02,-5.28 -9.27,-9.23 -2.25,-3.95 -3.37,-8.37 -3.37,-13.26s1.12,-9.31 3.37,-13.26c2.24,-3.95 5.33,-7.03 9.27,-9.23 3.93,-2.2 8.35,-3.3 13.27,-3.3s9.34,1.1 13.27,3.3c3.93,2.2 7.02,5.28 9.27,9.23 2.25,3.95 3.37,8.37 3.37,13.26s-1.12,9.31 -3.37,13.26Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M385.69,15.17l-13.69,0l0,73.39l43.84,0l8.39,-11.53l-38.54,0l0,-61.86z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M433.13,88.56l13.69,-18.95l0,-54.44l-13.69,0l0,73.39z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M257.47,15.17l-20.01,27.89l-19.8,-27.89l-15.58,0l54.44,73.39l14.85,0l-25.8,-35.44l27.7,-37.95l-15.8,0z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M503,50.61l25.8,-35.44l-14.85,0l-54.44,73.39l15.58,0l19.8,-27.89l20.01,27.89l15.8,0l-27.7,-37.95z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M202.07,88.57l12.88,0l19.6,0l-16.48,-22.22l-16,22.22z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M491.98,15.17l-12.88,0l-19.6,0l16.48,22.21l16,-22.21z" />
</vector>

View File

@ -1,12 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="80.52"
android:viewportHeight="80.46">
android:viewportWidth="103.75"
android:viewportHeight="103.75">
<path
android:fillColor="?colorPrimary"
android:pathData="M66.07,9.67A40,40 0,0 0,9.69 66.11Z" />
android:pathData="M83.34,14.62A48.75,48.75 0,0 0,14.62 83.41Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M13.92,70.34a40,40 0,0 0,56.45 -56.5Z" />
android:pathData="M19.77,88.57A48.76,48.76 0,0 0,88.58 19.7Z" />
</vector>

View File

@ -1,48 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="648dp"
android:height="80dp"
android:viewportWidth="648"
android:viewportHeight="80">
<path
android:fillColor="?colorPrimary"
android:pathData="M65.94,9.56A40,40 0,0 0,9.56 66Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M13.79,70.23a40,40 0,0 0,56.45 -56.5Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M144.75,28a9.12,9.12 0,0 0,-1 -4.48A8.81,8.81 0,0 0,141 20.29a14.14,14.14 0,0 0,-4 -1.95,15.61 15.61,0 0,0 -4.84,-0.68 15.34,15.34 0,0 0,-4.24 0.58,11.88 11.88,0 0,0 -3.55,1.66 7.15,7.15 0,0 0,-2.47 2.63,7.26 7.26,0 0,0 -0.88,3.61v0.49a7.11,7.11 0,0 0,0.88 3.7A6.91,6.91 0,0 0,124.53 33,19 19,0 0,0 129,34.82a58.17,58.17 0,0 0,6.31 1.27C140.9,37 145,38.52 147.61,41a12.31,12.31 0,0 1,3.94 9.66v0.87a16,16 0,0 1,-1.18 6.25,13.23 13.23,0 0,1 -3.55,4.87 17,17 0,0 1,-5.72 3.22,24.6 24.6,0 0,1 -8,1.17A26.21,26.21 0,0 1,124 65.54a18,18 0,0 1,-6.6 -4.1,18.51 18.51,0 0,1 -4.05,-6.14A21.41,21.41 0,0 1,112 47.59V45.35h6.41V47.2c0,4.68 1.28,8.1 3.85,10.44s6.11,3.51 10.75,3.51c4,0 7,-0.88 9,-2.63a8.82,8.82 0,0 0,3 -6.93V51.1a7.26,7.26 0,0 0,-3 -6.33q-3.12,-2.21 -10.06,-3.22a38.07,38.07 0,0 1,-7.3 -1.66,17.24 17.24,0 0,1 -5.53,-2.83 13.62,13.62 0,0 1,-3.55 -4.29,13.48 13.48,0 0,1 -1.18,-5.85V26a11.87,11.87 0,0 1,1.28 -5.65,13.37 13.37,0 0,1 3.65,-4.49A16.94,16.94 0,0 1,124.92 13a26.31,26.31 0,0 1,15.19 0.29,21.17 21.17,0 0,1 6,3.41 14.69,14.69 0,0 1,5 11.12V31h-6.41V28Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M161.91,12.68h33.93v5.85H182.13V59.59h13.71v5.85H161.91V59.59h13.71v-41H161.91Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M207.78,12.68h17.56q9.91,0 14.5,4.68c3,3.12 4.53,8.1 4.53,14.92L244.37,45.84c0,6.82 -1.48,11.8 -4.53,14.92s-7.89,4.68 -14.5,4.68L207.78,65.44L207.78,59.49h4.54L212.32,18.63h-4.54ZM224.45,59.68a22.74,22.74 0,0 0,6.11 -0.69,9.73 9.73,0 0,0 4.24,-2.34 9.43,9.43 0,0 0,2.47 -4.39,26.62 26.62,0 0,0 0.79,-6.82L238.06,32.77a26.2,26.2 0,0 0,-0.79 -6.83,10 10,0 0,0 -2.47,-4.38 9.73,9.73 0,0 0,-4.24 -2.34,22.26 22.26,0 0,0 -6.11,-0.69h-5.62L218.83,59.59h5.62Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M256.21,12.68h32.35v5.85H262.62V36.09h25.55v5.85H262.62V59.59h26.43v5.85H256.21Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M331.67,28a9.12,9.12 0,0 0,-1 -4.48,8.81 8.81,0 0,0 -2.76,-3.22 14.2,14.2 0,0 0,-4.05 -1.95,15.55 15.55,0 0,0 -4.83,-0.68 15.34,15.34 0,0 0,-4.24 0.58,11.77 11.77,0 0,0 -3.55,1.66 7.07,7.07 0,0 0,-2.47 2.63,7.26 7.26,0 0,0 -0.89,3.61v0.49a7.11,7.11 0,0 0,0.89 3.7A6.91,6.91 0,0 0,311.45 33a19,19 0,0 0,4.44 1.85,58.17 58.17,0 0,0 6.31,1.27c5.62,0.87 9.76,2.43 12.33,4.87a12.31,12.31 0,0 1,3.94 9.66v0.87a16,16 0,0 1,-1.18 6.25,13.23 13.23,0 0,1 -3.55,4.87A17,17 0,0 1,328 65.83,24.64 24.64,0 0,1 320,67 26.21,26.21 0,0 1,311 65.54a18.09,18.09 0,0 1,-6.61 -4.1,18.63 18.63,0 0,1 -4,-6.14 21.41,21.41 0,0 1,-1.38 -7.71V45.35h6.41V47.2c0,4.68 1.28,8.1 3.85,10.44s6.11,3.51 10.75,3.51c3.94,0 7,-0.88 9,-2.63A8.8,8.8 0,0 0,332 51.59V51.1a7.24,7.24 0,0 0,-3.06 -6.33q-3.11,-2.21 -10.06,-3.22a38,38 0,0 1,-7.29 -1.66A17.24,17.24 0,0 1,306 37.06a13.62,13.62 0,0 1,-3.55 -4.29,13.48 13.48,0 0,1 -1.18,-5.85V26a11.87,11.87 0,0 1,1.28 -5.65,13.37 13.37,0 0,1 3.65,-4.49A16.94,16.94 0,0 1,311.84 13a26.31,26.31 0,0 1,15.19 0.29,21.17 21.17,0 0,1 6,3.41 14.69,14.69 0,0 1,5 11.12V31h-6.41Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M348,12.68h6.41V36.09h22.79V12.68h6.41V65.54h-6.41V42H354.45V65.63H348Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M395.68,12.68h33.93v5.85H415.9V59.59h13.71v5.85H395.68V59.59h13.71v-41H395.68Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M443.23,12.68h32.84v5.85H449.64V36.09h25.94v5.85H449.64v23.6h-6.41V12.68Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M487.32,12.68H524.9v5.85H509.32v47H502.9V18.63H487.32V12.68Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M537.33,61.34a4.74,4.74 0,0 1,1.58 -3.7,5 5,0 0,1 3.74,-1.56A5.36,5.36 0,0 1,548 61.34a4.76,4.76 0,0 1,-1.58 3.71,5 5,0 0,1 -3.75,1.56 4.86,4.86 0,0 1,-3.74 -1.56A4.76,4.76 0,0 1,537.33 61.34Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M595.82,52.47H572.94l-3.85,13.16h-6.91l16.08,-52.95h12l16.17,52.86h-6.9ZM583.89,15.41l-9.28,31.11H594l-9.27,-31.11Z" />
<path
android:fillColor="?colorPrimary"
android:pathData="M614.07,12.68H648v5.85H634.29V59.59H648v5.85H614.07V59.59h13.71v-41H614.07Z" />
</vector>

View File

@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="663.62dp"
android:height="103.75dp"
android:viewportWidth="663.62"
android:viewportHeight="103.75">
<path
android:fillColor="?colorPrimary"
android:pathData="M73.82,21.31A40,40 0,0 0,17.44 77.75Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M21.67,82a40,40 0,0 0,56.45 -56.5Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M152.63,39.75a9.16,9.16 0,0 0,-1 -4.48A8.85,8.85 0,0 0,148.88 32a14.14,14.14 0,0 0,-4 -1.95,15.61 15.61,0 0,0 -4.84,-0.68 15.12,15.12 0,0 0,-4.24 0.58,11.64 11.64,0 0,0 -3.55,1.66 7.19,7.19 0,0 0,-3.35 6.24v0.49a7,7 0,0 0,0.88 3.7,6.89 6.89,0 0,0 2.63,2.67 18.74,18.74 0,0 0,4.47 1.82,58.24 58.24,0 0,0 6.31,1.27c5.59,0.91 9.69,2.43 12.3,4.91a12.32,12.32 0,0 1,3.94 9.66v0.87a16.3,16.3 0,0 1,-1.18 6.25,13.39 13.39,0 0,1 -3.55,4.87A17.14,17.14 0,0 1,149 77.62a24.7,24.7 0,0 1,-8 1.17,26.33 26.33,0 0,1 -9.1,-1.5 17.91,17.91 0,0 1,-6.6 -4.1,18.4 18.4,0 0,1 -4,-6.14 21.38,21.38 0,0 1,-1.35 -7.71V57.1h6.41V59c0,4.68 1.28,8.1 3.85,10.44s6.11,3.51 10.75,3.51q6,0 9,-2.63a8.88,8.88 0,0 0,3 -6.93v-0.49a7.28,7.28 0,0 0,-3 -6.33q-3.12,-2.21 -10.06,-3.22a37.86,37.86 0,0 1,-7.3 -1.66A17.16,17.16 0,0 1,127 48.81a13.49,13.49 0,0 1,-3.55 -4.29,13.41 13.41,0 0,1 -1.18,-5.85v-0.92a11.8,11.8 0,0 1,1.28 -5.65,13.25 13.25,0 0,1 3.65,-4.49 16.71,16.71 0,0 1,5.6 -2.86A26.29,26.29 0,0 1,148 25a21.12,21.12 0,0 1,6 3.41,14.73 14.73,0 0,1 5,11.12v3.18h-6.41v-3Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M169.79,24.43h33.93v5.85H190V71.34h13.71v5.85H169.79V71.34H183.5v-41H169.79Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M215.66,24.43h17.56q9.9,0 14.5,4.68c3,3.12 4.53,8.1 4.53,14.92L252.25,57.59c0,6.82 -1.48,11.8 -4.53,14.92s-7.89,4.68 -14.5,4.68L215.66,77.19v-6h4.54L220.2,30.38h-4.54ZM232.33,71.43a22.53,22.53 0,0 0,6.11 -0.69,9.86 9.86,0 0,0 4.24,-2.34A9.47,9.47 0,0 0,245.15 64a27,27 0,0 0,0.79 -6.82L245.94,44.52a26.63,26.63 0,0 0,-0.79 -6.83,10.07 10.07,0 0,0 -2.47,-4.38A9.76,9.76 0,0 0,238.44 31a22.53,22.53 0,0 0,-6.11 -0.69h-5.62L226.71,71.34h5.62Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M264.09,24.43h32.35v5.85H270.5V47.84h25.55v5.85H270.5V71.34h26.43v5.85H264.09Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M339.55,39.75a9.16,9.16 0,0 0,-1 -4.48,8.94 8.94,0 0,0 -2.76,-3.22 14.29,14.29 0,0 0,-4.05 -1.95,15.54 15.54,0 0,0 -4.83,-0.68 15.12,15.12 0,0 0,-4.24 0.58,11.64 11.64,0 0,0 -3.55,1.66 7,7 0,0 0,-2.47 2.63,7.13 7.13,0 0,0 -0.89,3.61v0.49a7,7 0,0 0,0.89 3.7,6.79 6.79,0 0,0 2.68,2.66 19,19 0,0 0,4.44 1.85,58.24 58.24,0 0,0 6.31,1.27c5.62,0.87 9.76,2.43 12.33,4.87a12.32,12.32 0,0 1,3.94 9.66v0.87a16.3,16.3 0,0 1,-1.18 6.25,13.39 13.39,0 0,1 -3.55,4.87 17,17 0,0 1,-5.74 3.19,24.7 24.7,0 0,1 -8,1.17 26.21,26.21 0,0 1,-9 -1.46,18 18,0 0,1 -6.61,-4.1 18.52,18.52 0,0 1,-4 -6.14,21.37 21.37,0 0,1 -1.38,-7.71V57.1h6.41V59c0,4.68 1.28,8.1 3.85,10.44s6.11,3.51 10.75,3.51c3.94,0 7,-0.88 9,-2.63a8.84,8.84 0,0 0,3 -6.93v-0.49a7.25,7.25 0,0 0,-3.06 -6.33q-3.12,-2.21 -10.06,-3.22a37.76,37.76 0,0 1,-7.29 -1.66,17 17,0 0,1 -5.59,-2.83 13.49,13.49 0,0 1,-3.55 -4.29,13.41 13.41,0 0,1 -1.18,-5.85v-0.92a11.8,11.8 0,0 1,1.28 -5.65,13.25 13.25,0 0,1 3.65,-4.49 16.8,16.8 0,0 1,5.64 -2.86,26.29 26.29,0 0,1 15.19,0.29 21.12,21.12 0,0 1,6 3.41,14.73 14.73,0 0,1 5,11.12v3.18H339.5Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M355.88,24.43h6.41V47.84h22.79V24.43h6.41V77.29h-6.41V53.75H362.33V77.38h-6.45Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M403.56,24.43h33.93v5.85H423.78V71.34h13.71v5.85H403.56V71.34h13.71v-41H403.56Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M451.11,24.43H484v5.85H457.52V47.84h25.94v5.85H457.52v23.6h-6.41Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M495.2,24.43h37.58v5.85H517.2v47h-6.42V30.38H495.2Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M545.21,73.09a4.73,4.73 0,0 1,1.58 -3.7,5 5,0 0,1 3.74,-1.56 5.36,5.36 0,0 1,5.35 5.26,4.79 4.79,0 0,1 -1.58,3.71 5,5 0,0 1,-3.75 1.56,4.85 4.85,0 0,1 -3.74,-1.56A4.74,4.74 0,0 1,545.21 73.09Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M603.7,64.22H580.82L577,77.38h-6.91l16.08,-52.95h12l16.17,52.86h-6.9ZM591.77,27.16l-9.28,31.11h19.39l-9.27,-31.11Z"/>
<path
android:fillColor="?colorPrimary"
android:pathData="M622,24.43h33.93v5.85H642.17V71.34h13.71v5.85H622V71.34h13.71v-41H622Z"/>
</vector>

View File

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#008CE7"
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" />
<path
android:fillColor="#FFF"
android:pathData="M19.086,8.004H11.81l-0.602,3.367 6.562,0.01c3.231,0 4.19,1.173 4.159,3.12 -0.014,0.998 -0.449,2.686 -0.633,3.23 -0.497,1.46 -1.521,3.122 -5.359,3.117l-6.378,-0.004 -0.602,3.371h7.257c2.559,0 3.649,-0.299 4.8,-0.83 2.554,-1.178 4.075,-3.701 4.686,-6.994 0.906,-4.9 -0.224,-8.387 -6.615,-8.387z" />
<path
android:fillColor="#FFF"
android:pathData="M15.807,15.798c0.237,-0.985 0.312,-1.38 0.312,-1.38H8.673c-1.904,0 -2.176,1.24 -2.357,1.99 -0.237,0.981 -0.312,1.381 -0.312,1.381h7.447c1.903,0 2.175,-1.24 2.356,-1.991z" />
</vector>

View File

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="@color/offForeground"
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" />
<path
android:fillColor="@color/offBackground"
android:pathData="M19.086,8.004H11.81l-0.602,3.367 6.562,0.01c3.231,0 4.19,1.173 4.159,3.12 -0.014,0.998 -0.449,2.686 -0.633,3.23 -0.497,1.46 -1.521,3.122 -5.359,3.117l-6.378,-0.004 -0.602,3.371h7.257c2.559,0 3.649,-0.299 4.8,-0.83 2.554,-1.178 4.075,-3.701 4.686,-6.994 0.906,-4.9 -0.224,-8.387 -6.615,-8.387z" />
<path
android:fillColor="@color/offBackground"
android:pathData="M15.807,15.798c0.237,-0.985 0.312,-1.38 0.312,-1.38H8.673c-1.904,0 -2.176,1.24 -2.357,1.99 -0.237,0.981 -0.312,1.381 -0.312,1.381h7.447c1.903,0 2.175,-1.24 2.356,-1.991z" />
</vector>

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