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:
parent
4ebcda2b14
commit
84ce392192
@ -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") + "\""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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) -> {
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -22,6 +22,5 @@ public interface NetworkCallback {
|
||||
|
||||
void onSuccess(JSONObject jsonObject);
|
||||
|
||||
void onError(Exception ex);
|
||||
|
||||
void onError(Exception ex, JSONObject json);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
34
app/src/main/java/com/m2049r/xmrwallet/util/IdHelper.java
Normal file
34
app/src/main/java/com/m2049r/xmrwallet/util/IdHelper.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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}};
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -400,7 +400,6 @@ public class ExchangeEditText extends LinearLayout {
|
||||
exchangeRate.getQuoteCurrency(), sCurrencyB.getSelectedItem());
|
||||
return;
|
||||
}
|
||||
|
||||
exchangeRateCache = exchangeRate;
|
||||
if (prepareExchange()) {
|
||||
exchange(exchangeRate.getRate());
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -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>
|
12
app/src/main/res/drawable-v24/ic_xmrto_sol_off.xml
Normal file
12
app/src/main/res/drawable-v24/ic_xmrto_sol_off.xml
Normal 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>
|
36
app/src/main/res/drawable-v24/ic_xmrto_usdt_trc20_off.xml
Normal file
36
app/src/main/res/drawable-v24/ic_xmrto_usdt_trc20_off.xml
Normal 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>
|
@ -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>
|
||||
|
12
app/src/main/res/drawable/ic_exolix_icon.xml
Normal file
12
app/src/main/res/drawable/ic_exolix_icon.xml
Normal 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>
|
30
app/src/main/res/drawable/ic_exolix_wide.xml
Normal file
30
app/src/main/res/drawable/ic_exolix_wide.xml
Normal 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>
|
@ -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>
|
@ -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>
|
48
app/src/main/res/drawable/ic_sideshift_wide.xml
Normal file
48
app/src/main/res/drawable/ic_sideshift_wide.xml
Normal 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>
|
@ -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>
|
@ -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
Loading…
Reference in New Issue
Block a user