mirror of
https://codeberg.org/r4v3r23/mysu.git
synced 2024-11-09 20:53:47 +01:00
Delete 99% of Monerujo code, replace with basic Monero wallet app to built from.
NOTE: As of this commit, the app logs the wallet seed for development purposes.
This commit is contained in:
parent
e6883a40d1
commit
b9a4ab18e1
214
LICENSE
214
LICENSE
@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2022 pokkst
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
|
51
README.md
51
README.md
@ -1,2 +1,49 @@
|
||||
# monero-wallet
|
||||
WIP Monero Wallet
|
||||
# Monerujo
|
||||
Another Android Monero Wallet for Monero
|
||||
**(not
|
||||
Monero Classic,
|
||||
Monero-Classic,
|
||||
Monero Zero,
|
||||
Monero Original,
|
||||
Monero C,
|
||||
Monero V)**
|
||||
|
||||
### QUICKSTART
|
||||
- Download the APK for the most current release [here](https://github.com/m2049r/xmrwallet/releases) and install it
|
||||
- Alternatively add our F-Droid repo https://f-droid.monerujo.io/fdroid/repo with fingerpint ```A8 2C 68 E1 4A F0 AA 6A 2E C2 0E 6B 27 2E FF 25 E5 A0 38 F3 F6 58 84 31 6E 0F 5E 0D 91 E7 B7 13``` to your F-Droid client
|
||||
- Run the App and select "Generate Wallet" to create a new wallet or recover a wallet
|
||||
- Advanced users can copy over synced wallet files (all files) onto sdcard in directory Monerujo (created first time App is started)
|
||||
- See the [FAQ](doc/FAQ.md)
|
||||
|
||||
## Translations
|
||||
Help us translate Monerujo! You can find instructions for adding a new translation or updating an existent one in [this guide](https://github.com/monero-ecosystem/monero-translations/blob/master/translate-monerujo.md), but is suggested to contact the Monero Localization Workgroup first if you have any doubt or question. You can do so in many ways. For example by email: translate@getmonero.org or chatting in `#monero-translations` (chatroom on Freenode, matrix and MatterMost). To see the complete list of contacts, take a look at the [official repository of the workgroup on GitHub](https://github.com/monero-ecosystem/monero-translations/blob/master/README.md#contacts).
|
||||
|
||||
### Disclaimer
|
||||
You may lose all your Moneroj if you use this App. Be cautious when spending on the mainnet.
|
||||
|
||||
### Random Notes
|
||||
- works on the mainnet & stagenet
|
||||
- use your own daemon - it's easy
|
||||
- Monerujo means "Monero Wallet" according to https://www.reddit.com/r/Monero/comments/3exy7t/esperanto_corner/
|
||||
|
||||
### TODO
|
||||
- see taiga.getmonero.org & issues on github
|
||||
|
||||
### Issues / Pitfalls
|
||||
- Users of Zenfone MAX & Zenfone 2 Laser (possibly others) **MUST** use the armeabi-v7a APK as the arm64-v8a build uses hardware AES
|
||||
functionality these models don't have.
|
||||
- You should backup your wallet files in the "monerujo" folder periodically.
|
||||
- Also note, that on some devices the backups will only be visible on a PC over USB after a reboot of the device (it's an Android bug/feature)
|
||||
- Created wallets on a private testnet are unusable because the restore height is set to that
|
||||
of the "real" testnet. After creating a new wallet, make a **new** one by recovering from the seed.
|
||||
The official monero client shows the same behaviour.
|
||||
|
||||
### HOW TO BUILD
|
||||
|
||||
See [the instructions](doc/BUILDING-external-libs.md)
|
||||
|
||||
Then, fire up Android Studio and build the APK.
|
||||
|
||||
### Donations
|
||||
- Address: 4AdkPJoxn7JCvAby9szgnt93MSEwdnxdhaASxbTBm6x5dCwmsDep2UYN4FhStDn5i11nsJbpU7oj59ahg8gXb1Mg3viqCuk
|
||||
- Viewkey: b1aff2a12191723da0afbe75516f94dd8b068215f6e847d8da57aca5f1f98e0c
|
||||
|
@ -112,6 +112,9 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
namespace 'com.m2049r.xmrwallet'
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
static def getId(name) {
|
||||
@ -146,6 +149,10 @@ dependencies {
|
||||
implementation 'org.jitsi:dnssecjava:1.2.0'
|
||||
implementation 'org.slf4j:slf4j-nop:1.7.36'
|
||||
implementation 'com.github.brnunes:swipeablerecyclerview:1.0.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.5.1'
|
||||
implementation 'androidx.navigation:navigation-ui:2.5.1'
|
||||
|
||||
//noinspection GradleDependency
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.m2049r.xmrwallet">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
@ -28,7 +29,6 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".XmrWalletApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@ -38,65 +38,13 @@
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".WalletActivity"
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
android:label="@string/wallet_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="behind" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="locked">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="monero" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<data android:scheme="bitcoin" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/usb_device_filter" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".onboarding.OnBoardingActivity"
|
||||
android:configChanges="orientation|keyboardHidden|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<service
|
||||
android:name=".service.WalletService"
|
||||
android:description="@string/service_description"
|
||||
android:exported="false"
|
||||
android:label="Monero Wallet Service" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
@ -1,313 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017-2020 m2049r et al.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.nfc.FormatException;
|
||||
import android.nfc.NdefMessage;
|
||||
import android.nfc.NdefRecord;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.nfc.Tag;
|
||||
import android.nfc.tech.Ndef;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.dialog.ProgressDialog;
|
||||
import com.m2049r.xmrwallet.fragment.send.SendFragment;
|
||||
import com.m2049r.xmrwallet.ledger.Ledger;
|
||||
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class BaseActivity extends SecureActivity
|
||||
implements GenerateReviewFragment.ProgressListener, SubaddressFragment.ProgressListener {
|
||||
|
||||
ProgressDialog progressDialog = null;
|
||||
|
||||
private class SimpleProgressDialog extends ProgressDialog {
|
||||
|
||||
SimpleProgressDialog(Context context, int msgId) {
|
||||
super(context);
|
||||
setCancelable(false);
|
||||
setMessage(context.getString(msgId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// prevent back button
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showProgressDialog(int msgId) {
|
||||
showProgressDialog(msgId, 250); // don't show dialog for fast operations
|
||||
}
|
||||
|
||||
public void showProgressDialog(int msgId, long delayMillis) {
|
||||
dismissProgressDialog(); // just in case
|
||||
progressDialog = new SimpleProgressDialog(BaseActivity.this, msgId);
|
||||
if (delayMillis > 0) {
|
||||
Handler handler = new Handler();
|
||||
handler.postDelayed(new Runnable() {
|
||||
public void run() {
|
||||
if (progressDialog != null) progressDialog.show();
|
||||
}
|
||||
}, delayMillis);
|
||||
} else {
|
||||
progressDialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLedgerProgressDialog(int mode) {
|
||||
dismissProgressDialog(); // just in case
|
||||
progressDialog = new LedgerProgressDialog(BaseActivity.this, mode);
|
||||
Ledger.setListener((Ledger.Listener) progressDialog);
|
||||
progressDialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismissProgressDialog() {
|
||||
if (progressDialog == null) return; // nothing to do
|
||||
if (progressDialog instanceof Ledger.Listener) {
|
||||
Ledger.unsetListener((Ledger.Listener) progressDialog);
|
||||
}
|
||||
if (progressDialog.isShowing()) {
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
progressDialog = null;
|
||||
}
|
||||
|
||||
static final int RELEASE_WAKE_LOCK_DELAY = 5000; // millisconds
|
||||
|
||||
private PowerManager.WakeLock wl = null;
|
||||
|
||||
void acquireWakeLock() {
|
||||
if ((wl != null) && wl.isHeld()) return;
|
||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
this.wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getString(R.string.app_name));
|
||||
try {
|
||||
wl.acquire();
|
||||
Timber.d("WakeLock acquired");
|
||||
} catch (SecurityException ex) {
|
||||
Timber.w("WakeLock NOT acquired: %s", ex.getLocalizedMessage());
|
||||
wl = null;
|
||||
}
|
||||
}
|
||||
|
||||
void releaseWakeLock(int delayMillis) {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
releaseWakeLock();
|
||||
}
|
||||
}, delayMillis);
|
||||
}
|
||||
|
||||
void releaseWakeLock() {
|
||||
if ((wl == null) || !wl.isHeld()) return;
|
||||
wl.release();
|
||||
wl = null;
|
||||
Timber.d("WakeLock released");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
initNfc();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
if (nfcAdapter != null) {
|
||||
nfcAdapter.enableForegroundDispatch(this, nfcPendingIntent, null, null);
|
||||
// intercept all techs so we can tell the user their tag is no good
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Timber.d("onPause()");
|
||||
if (nfcAdapter != null)
|
||||
nfcAdapter.disableForegroundDispatch(this);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
processNfcIntent(intent);
|
||||
}
|
||||
|
||||
// NFC stuff
|
||||
private NfcAdapter nfcAdapter;
|
||||
private PendingIntent nfcPendingIntent;
|
||||
|
||||
public void initNfc() {
|
||||
nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
if (nfcAdapter == null) // no NFC support
|
||||
return;
|
||||
nfcPendingIntent = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
}
|
||||
|
||||
private void processNfcIntent(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
Timber.d("ACTION=%s", action);
|
||||
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
|
||||
|| NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
|
||||
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {
|
||||
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||
Ndef ndef = Ndef.get(tag);
|
||||
if (ndef == null) {
|
||||
Toast.makeText(this, getString(R.string.nfc_tag_unsupported), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
|
||||
if (f instanceof ReceiveFragment) {
|
||||
// We want to write a Tag from the ReceiveFragment
|
||||
BarcodeData bc = ((ReceiveFragment) f).getBarcodeData();
|
||||
if (bc != null) {
|
||||
new AsyncWriteTag(ndef, bc.getUri()).execute();
|
||||
} // else wallet is not loaded yet or receive is otherwise not ready - ignore
|
||||
} else if (f instanceof SendFragment) {
|
||||
// We want to read a Tag for the SendFragment
|
||||
NdefMessage ndefMessage = ndef.getCachedNdefMessage();
|
||||
if (ndefMessage == null) {
|
||||
Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
NdefRecord firstRecord = ndefMessage.getRecords()[0];
|
||||
Uri uri = firstRecord.toUri(); // we insist on the first record
|
||||
if (uri == null) {
|
||||
Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
BarcodeData bc = BarcodeData.fromString(uri.toString());
|
||||
if (bc == null)
|
||||
Toast.makeText(this, getString(R.string.nfc_tag_read_undef), Toast.LENGTH_LONG).show();
|
||||
else
|
||||
onUriScanned(bc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this gets called only if we get data
|
||||
@CallSuper
|
||||
void onUriScanned(BarcodeData barcodeData) {
|
||||
// do nothing by default yet
|
||||
}
|
||||
|
||||
private BarcodeData barcodeData = null;
|
||||
|
||||
private BarcodeData popBarcodeData() {
|
||||
BarcodeData popped = barcodeData;
|
||||
barcodeData = null;
|
||||
return popped;
|
||||
}
|
||||
|
||||
private class AsyncWriteTag extends AsyncTask<Void, Void, Boolean> {
|
||||
|
||||
Ndef ndef;
|
||||
Uri uri;
|
||||
String errorMessage = null;
|
||||
|
||||
AsyncWriteTag(Ndef ndef, Uri uri) {
|
||||
this.ndef = ndef;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
showProgressDialog(R.string.progress_nfc_write);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
if (params.length != 0) return false;
|
||||
try {
|
||||
writeNdef(ndef, uri);
|
||||
return true;
|
||||
} catch (IOException | FormatException ex) {
|
||||
Timber.e(ex);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
errorMessage = ex.getMessage();
|
||||
Timber.d(errorMessage);
|
||||
} finally {
|
||||
try {
|
||||
ndef.close();
|
||||
} catch (IOException ex) {
|
||||
Timber.e(ex);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
if (isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
dismissProgressDialog();
|
||||
if (!result) {
|
||||
if (errorMessage != null)
|
||||
Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show();
|
||||
else
|
||||
Toast.makeText(getApplicationContext(), getString(R.string.nfc_write_failed), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(getApplicationContext(), getString(R.string.nfc_write_successful), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void writeNdef(Ndef ndef, Uri uri) throws IOException, FormatException {
|
||||
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||
if (nfcAdapter == null) return; // no NFC support here
|
||||
|
||||
NdefRecord recordNFC = NdefRecord.createUri(uri);
|
||||
NdefMessage message = new NdefMessage(recordNFC);
|
||||
ndef.connect();
|
||||
int tagSize = ndef.getMaxSize();
|
||||
int msgSize = message.getByteArrayLength();
|
||||
Timber.d("tagSize=%d, msgSIze=%d, uriSize=%d", tagSize, msgSize, uri.toString().length());
|
||||
if (tagSize < msgSize)
|
||||
throw new IllegalArgumentException(getString(R.string.nfc_tag_size, tagSize, msgSize));
|
||||
ndef.writeNdefMessage(message);
|
||||
}
|
||||
}
|
@ -1,615 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.FingerprintHelper;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.RestoreHeight;
|
||||
import com.m2049r.xmrwallet.util.ledger.Monero;
|
||||
import com.m2049r.xmrwallet.widget.PasswordEntryView;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class GenerateFragment extends Fragment {
|
||||
|
||||
static final String TYPE = "type";
|
||||
static final String TYPE_NEW = "new";
|
||||
static final String TYPE_KEY = "key";
|
||||
static final String TYPE_SEED = "seed";
|
||||
static final String TYPE_LEDGER = "ledger";
|
||||
static final String TYPE_VIEWONLY = "view";
|
||||
|
||||
private TextInputLayout etWalletName;
|
||||
private PasswordEntryView etWalletPassword;
|
||||
private LinearLayout llFingerprintAuth;
|
||||
private TextInputLayout etWalletAddress;
|
||||
private TextInputLayout etWalletMnemonic;
|
||||
private TextInputLayout etWalletViewKey;
|
||||
private TextInputLayout etWalletSpendKey;
|
||||
private TextInputLayout etWalletRestoreHeight;
|
||||
private Button bGenerate;
|
||||
|
||||
private Button bSeedOffset;
|
||||
private TextInputLayout etSeedOffset;
|
||||
|
||||
private String type = null;
|
||||
|
||||
private void clearErrorOnTextEntry(final TextInputLayout textInputLayout) {
|
||||
textInputLayout.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
textInputLayout.setError(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Bundle args = getArguments();
|
||||
this.type = args.getString(TYPE);
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_generate, container, false);
|
||||
|
||||
etWalletName = view.findViewById(R.id.etWalletName);
|
||||
etWalletPassword = view.findViewById(R.id.etWalletPassword);
|
||||
llFingerprintAuth = view.findViewById(R.id.llFingerprintAuth);
|
||||
etWalletMnemonic = view.findViewById(R.id.etWalletMnemonic);
|
||||
etWalletAddress = view.findViewById(R.id.etWalletAddress);
|
||||
etWalletViewKey = view.findViewById(R.id.etWalletViewKey);
|
||||
etWalletSpendKey = view.findViewById(R.id.etWalletSpendKey);
|
||||
etWalletRestoreHeight = view.findViewById(R.id.etWalletRestoreHeight);
|
||||
bGenerate = view.findViewById(R.id.bGenerate);
|
||||
bSeedOffset = view.findViewById(R.id.bSeedOffset);
|
||||
etSeedOffset = view.findViewById(R.id.etSeedOffset);
|
||||
|
||||
etWalletAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
etWalletViewKey.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
etWalletSpendKey.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
|
||||
etWalletName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
checkName();
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletName);
|
||||
|
||||
etWalletMnemonic.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
checkMnemonic();
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletMnemonic);
|
||||
|
||||
etWalletAddress.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
checkAddress();
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletAddress);
|
||||
|
||||
etWalletViewKey.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
checkViewKey();
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletViewKey);
|
||||
|
||||
etWalletSpendKey.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
checkSpendKey();
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletSpendKey);
|
||||
|
||||
Helper.showKeyboard(requireActivity());
|
||||
|
||||
etWalletName.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
|
||||
if (checkName()) {
|
||||
etWalletPassword.requestFocus();
|
||||
} // otherwise ignore
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (FingerprintHelper.isDeviceSupported(getContext())) {
|
||||
llFingerprintAuth.setVisibility(View.VISIBLE);
|
||||
|
||||
final SwitchMaterial swFingerprintAllowed = (SwitchMaterial) llFingerprintAuth.getChildAt(0);
|
||||
swFingerprintAllowed.setOnClickListener(view1 -> {
|
||||
if (!swFingerprintAllowed.isChecked()) return;
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||
builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn)))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel), (dialogInterface, i) -> swFingerprintAllowed.setChecked(false))
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case TYPE_NEW:
|
||||
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
|
||||
break;
|
||||
case TYPE_LEDGER:
|
||||
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
etWalletRestoreHeight.requestFocus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
break;
|
||||
case TYPE_SEED:
|
||||
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
|
||||
etWalletMnemonic.requestFocus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
etWalletMnemonic.setVisibility(View.VISIBLE);
|
||||
etWalletMnemonic.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
|
||||
if (checkMnemonic()) {
|
||||
etWalletRestoreHeight.requestFocus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
bSeedOffset.setVisibility(View.VISIBLE);
|
||||
bSeedOffset.setOnClickListener(v -> toggleSeedOffset());
|
||||
break;
|
||||
case TYPE_KEY:
|
||||
case TYPE_VIEWONLY:
|
||||
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
|
||||
etWalletAddress.requestFocus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
etWalletAddress.setVisibility(View.VISIBLE);
|
||||
etWalletAddress.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
|
||||
if (checkAddress()) {
|
||||
etWalletViewKey.requestFocus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
etWalletViewKey.setVisibility(View.VISIBLE);
|
||||
etWalletViewKey.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
|
||||
if (checkViewKey()) {
|
||||
if (type.equals(TYPE_KEY)) {
|
||||
etWalletSpendKey.requestFocus();
|
||||
} else {
|
||||
etWalletRestoreHeight.requestFocus();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (type.equals(TYPE_KEY)) {
|
||||
etWalletSpendKey.setVisibility(View.VISIBLE);
|
||||
etWalletSpendKey.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_NEXT)) {
|
||||
if (checkSpendKey()) {
|
||||
etWalletRestoreHeight.requestFocus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
if (!type.equals(TYPE_NEW)) {
|
||||
etWalletRestoreHeight.setVisibility(View.VISIBLE);
|
||||
etWalletRestoreHeight.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
|
||||
}
|
||||
bGenerate.setOnClickListener(v -> {
|
||||
Helper.hideKeyboard(getActivity());
|
||||
generateWallet();
|
||||
});
|
||||
|
||||
etWalletName.requestFocus();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
void toggleSeedOffset() {
|
||||
if (etSeedOffset.getVisibility() == View.VISIBLE) {
|
||||
etSeedOffset.getEditText().getText().clear();
|
||||
etSeedOffset.setVisibility(View.GONE);
|
||||
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0);
|
||||
} else {
|
||||
etSeedOffset.setVisibility(View.VISIBLE);
|
||||
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0);
|
||||
etSeedOffset.requestFocusFromTouch();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkName() {
|
||||
String name = etWalletName.getEditText().getText().toString();
|
||||
boolean ok = true;
|
||||
if (name.length() == 0) {
|
||||
etWalletName.setError(getString(R.string.generate_wallet_name));
|
||||
ok = false;
|
||||
} else if (name.charAt(0) == '.') {
|
||||
etWalletName.setError(getString(R.string.generate_wallet_dot));
|
||||
ok = false;
|
||||
} else {
|
||||
File walletFile = Helper.getWalletFile(getActivity(), name);
|
||||
if (WalletManager.getInstance().walletExists(walletFile)) {
|
||||
etWalletName.setError(getString(R.string.generate_wallet_exists));
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
etWalletName.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkHeight() {
|
||||
long height = !type.equals(TYPE_NEW) ? getHeight() : 0;
|
||||
boolean ok = true;
|
||||
if (height < 0) {
|
||||
etWalletRestoreHeight.setError(getString(R.string.generate_restoreheight_error));
|
||||
ok = false;
|
||||
}
|
||||
if (ok) {
|
||||
etWalletRestoreHeight.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private long getHeight() {
|
||||
long height = -1;
|
||||
|
||||
String restoreHeight = etWalletRestoreHeight.getEditText().getText().toString().trim();
|
||||
if (restoreHeight.isEmpty()) return -1;
|
||||
try {
|
||||
// is it a date?
|
||||
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
|
||||
parser.setLenient(false);
|
||||
height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight));
|
||||
} catch (ParseException ignored) {
|
||||
}
|
||||
if ((height < 0) && (restoreHeight.length() == 8))
|
||||
try {
|
||||
// is it a date without dashes?
|
||||
SimpleDateFormat parser = new SimpleDateFormat("yyyyMMdd");
|
||||
parser.setLenient(false);
|
||||
height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight));
|
||||
} catch (ParseException ignored) {
|
||||
}
|
||||
if (height < 0)
|
||||
try {
|
||||
// or is it a height?
|
||||
height = Long.parseLong(restoreHeight);
|
||||
} catch (NumberFormatException ex) {
|
||||
return -1;
|
||||
}
|
||||
Timber.d("Using Restore Height = %d", height);
|
||||
return height;
|
||||
}
|
||||
|
||||
private boolean checkMnemonic() {
|
||||
String seed = etWalletMnemonic.getEditText().getText().toString();
|
||||
boolean ok = (seed.split("\\s").length == 25); // 25 words
|
||||
if (!ok) {
|
||||
etWalletMnemonic.setError(getString(R.string.generate_check_mnemonic));
|
||||
} else {
|
||||
etWalletMnemonic.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkAddress() {
|
||||
String address = etWalletAddress.getEditText().getText().toString();
|
||||
boolean ok = Wallet.isAddressValid(address);
|
||||
if (!ok) {
|
||||
etWalletAddress.setError(getString(R.string.generate_check_address));
|
||||
} else {
|
||||
etWalletAddress.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkViewKey() {
|
||||
String viewKey = etWalletViewKey.getEditText().getText().toString();
|
||||
boolean ok = (viewKey.length() == 64) && (viewKey.matches("^[0-9a-fA-F]+$"));
|
||||
if (!ok) {
|
||||
etWalletViewKey.setError(getString(R.string.generate_check_key));
|
||||
} else {
|
||||
etWalletViewKey.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean checkSpendKey() {
|
||||
String spendKey = etWalletSpendKey.getEditText().getText().toString();
|
||||
boolean ok = ((spendKey.length() == 0) || ((spendKey.length() == 64) && (spendKey.matches("^[0-9a-fA-F]+$"))));
|
||||
if (!ok) {
|
||||
etWalletSpendKey.setError(getString(R.string.generate_check_key));
|
||||
} else {
|
||||
etWalletSpendKey.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private void generateWallet() {
|
||||
if (!checkName()) return;
|
||||
if (!checkHeight()) return;
|
||||
|
||||
String name = etWalletName.getEditText().getText().toString();
|
||||
String password = etWalletPassword.getEditText().getText().toString();
|
||||
boolean fingerprintAuthAllowed = ((SwitchMaterial) llFingerprintAuth.getChildAt(0)).isChecked();
|
||||
|
||||
// create the real wallet password
|
||||
String crazyPass = KeyStoreHelper.getCrazyPass(getActivity(), password);
|
||||
|
||||
long height = getHeight();
|
||||
if (height < 0) height = 0;
|
||||
|
||||
switch (type) {
|
||||
case TYPE_NEW:
|
||||
bGenerate.setEnabled(false);
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
|
||||
}
|
||||
activityCallback.onGenerate(name, crazyPass);
|
||||
break;
|
||||
case TYPE_SEED:
|
||||
if (!checkMnemonic()) return;
|
||||
final String seed = etWalletMnemonic.getEditText().getText().toString();
|
||||
bGenerate.setEnabled(false);
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
|
||||
}
|
||||
final String offset = etSeedOffset.getEditText().getText().toString();
|
||||
activityCallback.onGenerate(name, crazyPass, seed, offset, height);
|
||||
break;
|
||||
case TYPE_LEDGER:
|
||||
bGenerate.setEnabled(false);
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
|
||||
}
|
||||
activityCallback.onGenerateLedger(name, crazyPass, height);
|
||||
break;
|
||||
case TYPE_KEY:
|
||||
case TYPE_VIEWONLY:
|
||||
if (checkAddress() && checkViewKey() && checkSpendKey()) {
|
||||
bGenerate.setEnabled(false);
|
||||
String address = etWalletAddress.getEditText().getText().toString();
|
||||
String viewKey = etWalletViewKey.getEditText().getText().toString();
|
||||
String spendKey = "";
|
||||
if (type.equals(TYPE_KEY)) {
|
||||
spendKey = etWalletSpendKey.getEditText().getText().toString();
|
||||
}
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
|
||||
}
|
||||
activityCallback.onGenerate(name, crazyPass, address, viewKey, spendKey, height);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void walletGenerateError() {
|
||||
bGenerate.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activityCallback.setTitle(getString(R.string.generate_title) + " - " + getType());
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
|
||||
}
|
||||
|
||||
String getType() {
|
||||
switch (type) {
|
||||
case TYPE_KEY:
|
||||
return getString(R.string.generate_wallet_type_key);
|
||||
case TYPE_NEW:
|
||||
return getString(R.string.generate_wallet_type_new);
|
||||
case TYPE_SEED:
|
||||
return getString(R.string.generate_wallet_type_seed);
|
||||
case TYPE_LEDGER:
|
||||
return getString(R.string.generate_wallet_type_ledger);
|
||||
case TYPE_VIEWONLY:
|
||||
return getString(R.string.generate_wallet_type_view);
|
||||
default:
|
||||
Timber.e("unknown type %s", type);
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
GenerateFragment.Listener activityCallback;
|
||||
|
||||
public interface Listener {
|
||||
void onGenerate(String name, String password);
|
||||
|
||||
void onGenerate(String name, String password, String seed, String offset, long height);
|
||||
|
||||
void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height);
|
||||
|
||||
void onGenerateLedger(String name, String password, long height);
|
||||
|
||||
void setTitle(String title);
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof GenerateFragment.Listener) {
|
||||
this.activityCallback = (GenerateFragment.Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
switch (type) {
|
||||
case TYPE_KEY:
|
||||
inflater.inflate(R.menu.create_wallet_keys, menu);
|
||||
break;
|
||||
case TYPE_NEW:
|
||||
inflater.inflate(R.menu.create_wallet_new, menu);
|
||||
break;
|
||||
case TYPE_SEED:
|
||||
inflater.inflate(R.menu.create_wallet_seed, menu);
|
||||
break;
|
||||
case TYPE_LEDGER:
|
||||
inflater.inflate(R.menu.create_wallet_ledger, menu);
|
||||
break;
|
||||
case TYPE_VIEWONLY:
|
||||
inflater.inflate(R.menu.create_wallet_view, menu);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
AlertDialog ledgerDialog = null;
|
||||
|
||||
public void convertLedgerSeed() {
|
||||
if (ledgerDialog != null) return;
|
||||
final Activity activity = requireActivity();
|
||||
View promptsView = getLayoutInflater().inflate(R.layout.prompt_ledger_seed, null);
|
||||
MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(activity);
|
||||
alertDialogBuilder.setView(promptsView);
|
||||
|
||||
final TextInputLayout etSeed = promptsView.findViewById(R.id.etSeed);
|
||||
final TextInputLayout etPassphrase = promptsView.findViewById(R.id.etPassphrase);
|
||||
|
||||
clearErrorOnTextEntry(etSeed);
|
||||
|
||||
alertDialogBuilder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Helper.hideKeyboardAlways(activity);
|
||||
etWalletMnemonic.getEditText().getText().clear();
|
||||
dialog.cancel();
|
||||
ledgerDialog = null;
|
||||
}
|
||||
});
|
||||
|
||||
ledgerDialog = alertDialogBuilder.create();
|
||||
|
||||
ledgerDialog.setOnShowListener(dialog -> {
|
||||
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(view -> {
|
||||
String ledgerSeed = etSeed.getEditText().getText().toString();
|
||||
String ledgerPassphrase = etPassphrase.getEditText().getText().toString();
|
||||
String moneroSeed = Monero.convert(ledgerSeed, ledgerPassphrase);
|
||||
if (moneroSeed != null) {
|
||||
etWalletMnemonic.getEditText().setText(moneroSeed);
|
||||
ledgerDialog.dismiss();
|
||||
ledgerDialog = null;
|
||||
} else {
|
||||
etSeed.setError(getString(R.string.bad_ledger_seed));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (Helper.preventScreenshot()) {
|
||||
ledgerDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
ledgerDialog.show();
|
||||
}
|
||||
}
|
@ -1,711 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.ledger.Ledger;
|
||||
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.FingerprintHelper;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
|
||||
import com.m2049r.xmrwallet.widget.PasswordEntryView;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class GenerateReviewFragment extends Fragment {
|
||||
static final public String VIEW_TYPE_DETAILS = "details";
|
||||
static final public String VIEW_TYPE_ACCEPT = "accept";
|
||||
static final public String VIEW_TYPE_WALLET = "wallet";
|
||||
|
||||
public static final String REQUEST_TYPE = "type";
|
||||
public static final String REQUEST_PATH = "path";
|
||||
public static final String REQUEST_PASSWORD = "password";
|
||||
|
||||
private ScrollView scrollview;
|
||||
|
||||
private ProgressBar pbProgress;
|
||||
private TextView tvWalletPassword;
|
||||
private TextView tvWalletAddress;
|
||||
private FrameLayout flWalletMnemonic;
|
||||
private TextView tvWalletMnemonic;
|
||||
private TextView tvWalletHeight;
|
||||
private TextView tvWalletViewKey;
|
||||
private TextView tvWalletSpendKey;
|
||||
private ImageButton bCopyAddress;
|
||||
private LinearLayout llAdvancedInfo;
|
||||
private LinearLayout llPassword;
|
||||
private LinearLayout llMnemonic;
|
||||
private LinearLayout llSpendKey;
|
||||
private LinearLayout llViewKey;
|
||||
private Button bAdvancedInfo;
|
||||
private Button bAccept;
|
||||
|
||||
private Button bSeedOffset;
|
||||
private TextInputLayout etSeedOffset;
|
||||
|
||||
private String walletPath;
|
||||
private String walletName;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_review, container, false);
|
||||
|
||||
scrollview = view.findViewById(R.id.scrollview);
|
||||
pbProgress = view.findViewById(R.id.pbProgress);
|
||||
tvWalletPassword = view.findViewById(R.id.tvWalletPassword);
|
||||
tvWalletAddress = view.findViewById(R.id.tvWalletAddress);
|
||||
tvWalletViewKey = view.findViewById(R.id.tvWalletViewKey);
|
||||
tvWalletSpendKey = view.findViewById(R.id.tvWalletSpendKey);
|
||||
tvWalletMnemonic = view.findViewById(R.id.tvWalletMnemonic);
|
||||
flWalletMnemonic = view.findViewById(R.id.flWalletMnemonic);
|
||||
tvWalletHeight = view.findViewById(R.id.tvWalletHeight);
|
||||
bCopyAddress = view.findViewById(R.id.bCopyAddress);
|
||||
bAdvancedInfo = view.findViewById(R.id.bAdvancedInfo);
|
||||
llAdvancedInfo = view.findViewById(R.id.llAdvancedInfo);
|
||||
llPassword = view.findViewById(R.id.llPassword);
|
||||
llMnemonic = view.findViewById(R.id.llMnemonic);
|
||||
llSpendKey = view.findViewById(R.id.llSpendKey);
|
||||
llViewKey = view.findViewById(R.id.llViewKey);
|
||||
|
||||
etSeedOffset = view.findViewById(R.id.etSeedOffset);
|
||||
bSeedOffset = view.findViewById(R.id.bSeedOffset);
|
||||
|
||||
bAccept = view.findViewById(R.id.bAccept);
|
||||
|
||||
boolean allowCopy = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet;
|
||||
tvWalletMnemonic.setTextIsSelectable(allowCopy);
|
||||
tvWalletSpendKey.setTextIsSelectable(allowCopy);
|
||||
tvWalletPassword.setTextIsSelectable(allowCopy);
|
||||
|
||||
bAccept.setOnClickListener(v -> acceptWallet());
|
||||
view.findViewById(R.id.bCopyViewKey).setOnClickListener(v -> copyViewKey());
|
||||
bCopyAddress.setEnabled(false);
|
||||
bCopyAddress.setOnClickListener(v -> copyAddress());
|
||||
bAdvancedInfo.setOnClickListener(v -> toggleAdvancedInfo());
|
||||
|
||||
bSeedOffset.setOnClickListener(v -> toggleSeedOffset());
|
||||
etSeedOffset.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
showSeed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
Bundle args = getArguments();
|
||||
type = args.getString(REQUEST_TYPE);
|
||||
walletPath = args.getString(REQUEST_PATH);
|
||||
localPassword = args.getString(REQUEST_PASSWORD);
|
||||
showDetails();
|
||||
return view;
|
||||
}
|
||||
|
||||
String getSeedOffset() {
|
||||
return etSeedOffset.getEditText().getText().toString();
|
||||
}
|
||||
|
||||
boolean seedOffsetInProgress = false;
|
||||
|
||||
void showSeed() {
|
||||
synchronized (this) {
|
||||
if (seedOffsetInProgress) return;
|
||||
seedOffsetInProgress = true;
|
||||
}
|
||||
new AsyncShowSeed().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath);
|
||||
}
|
||||
|
||||
void showDetails() {
|
||||
tvWalletPassword.setText(null);
|
||||
new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath);
|
||||
}
|
||||
|
||||
void copyViewKey() {
|
||||
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_viewkey), tvWalletViewKey.getText().toString());
|
||||
Toast.makeText(getActivity(), getString(R.string.message_copy_viewkey), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
void copyAddress() {
|
||||
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_address), tvWalletAddress.getText().toString());
|
||||
Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
void nocopy() {
|
||||
Toast.makeText(getActivity(), getString(R.string.message_nocopy), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
void toggleAdvancedInfo() {
|
||||
if (llAdvancedInfo.getVisibility() == View.VISIBLE) {
|
||||
llAdvancedInfo.setVisibility(View.GONE);
|
||||
bAdvancedInfo.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0);
|
||||
} else {
|
||||
llAdvancedInfo.setVisibility(View.VISIBLE);
|
||||
bAdvancedInfo.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0);
|
||||
scrollview.post(() -> scrollview.fullScroll(ScrollView.FOCUS_DOWN));
|
||||
}
|
||||
}
|
||||
|
||||
void toggleSeedOffset() {
|
||||
if (etSeedOffset.getVisibility() == View.VISIBLE) {
|
||||
etSeedOffset.getEditText().getText().clear();
|
||||
etSeedOffset.setVisibility(View.GONE);
|
||||
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_down, 0, 0, 0);
|
||||
} else {
|
||||
etSeedOffset.setVisibility(View.VISIBLE);
|
||||
bSeedOffset.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_keyboard_arrow_up, 0, 0, 0);
|
||||
etSeedOffset.requestFocusFromTouch();
|
||||
}
|
||||
}
|
||||
|
||||
String type;
|
||||
|
||||
private void acceptWallet() {
|
||||
bAccept.setEnabled(false);
|
||||
acceptCallback.onAccept(walletName, getPassword());
|
||||
}
|
||||
|
||||
private class AsyncShow extends AsyncTask<String, Void, Boolean> {
|
||||
String name;
|
||||
String address;
|
||||
long height;
|
||||
String seed;
|
||||
String viewKey;
|
||||
String spendKey;
|
||||
Wallet.Status walletStatus;
|
||||
|
||||
boolean dialogOpened = false;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
showProgress();
|
||||
if ((walletPath != null)
|
||||
&& (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", getPassword())
|
||||
== Wallet.Device.Device_Ledger)
|
||||
&& (progressCallback != null)) {
|
||||
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
|
||||
dialogOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(String... params) {
|
||||
if (params.length != 1) return false;
|
||||
String walletPath = params[0];
|
||||
|
||||
Wallet wallet;
|
||||
boolean closeWallet;
|
||||
if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) {
|
||||
wallet = GenerateReviewFragment.this.walletCallback.getWallet();
|
||||
closeWallet = false;
|
||||
} else {
|
||||
wallet = WalletManager.getInstance().openWallet(walletPath, getPassword());
|
||||
closeWallet = true;
|
||||
}
|
||||
name = wallet.getName();
|
||||
walletStatus = wallet.getStatus();
|
||||
if (!walletStatus.isOk()) {
|
||||
if (closeWallet) wallet.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
address = wallet.getAddress();
|
||||
height = wallet.getRestoreHeight();
|
||||
seed = wallet.getSeed(getSeedOffset());
|
||||
switch (wallet.getDeviceType()) {
|
||||
case Device_Ledger:
|
||||
viewKey = Ledger.Key();
|
||||
break;
|
||||
case Device_Software:
|
||||
viewKey = wallet.getSecretViewKey();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Hardware backing not supported. At all!");
|
||||
}
|
||||
spendKey = wallet.isWatchOnly() ? getActivity().getString(R.string.label_watchonly) : wallet.getSecretSpendKey();
|
||||
if (closeWallet) wallet.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
if (dialogOpened)
|
||||
progressCallback.dismissProgressDialog();
|
||||
if (!isAdded()) return; // never mind
|
||||
walletName = name;
|
||||
if (result) {
|
||||
if (type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT)) {
|
||||
bAccept.setVisibility(View.VISIBLE);
|
||||
bAccept.setEnabled(true);
|
||||
}
|
||||
llPassword.setVisibility(View.VISIBLE);
|
||||
tvWalletPassword.setText(getPassword());
|
||||
tvWalletAddress.setText(address);
|
||||
tvWalletHeight.setText(NumberFormat.getInstance().format(height));
|
||||
if (!seed.isEmpty()) {
|
||||
llMnemonic.setVisibility(View.VISIBLE);
|
||||
tvWalletMnemonic.setText(seed);
|
||||
}
|
||||
boolean showAdvanced = false;
|
||||
if (isKeyValid(viewKey)) {
|
||||
llViewKey.setVisibility(View.VISIBLE);
|
||||
tvWalletViewKey.setText(viewKey);
|
||||
showAdvanced = true;
|
||||
}
|
||||
if (isKeyValid(spendKey)) {
|
||||
llSpendKey.setVisibility(View.VISIBLE);
|
||||
tvWalletSpendKey.setText(spendKey);
|
||||
showAdvanced = true;
|
||||
}
|
||||
if (showAdvanced) bAdvancedInfo.setVisibility(View.VISIBLE);
|
||||
bCopyAddress.setEnabled(true);
|
||||
activityCallback.setTitle(name, getString(R.string.details_title));
|
||||
activityCallback.setToolbarButton(
|
||||
GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK);
|
||||
} else {
|
||||
// TODO show proper error message and/or end the fragment?
|
||||
tvWalletAddress.setText(walletStatus.toString());
|
||||
tvWalletHeight.setText(walletStatus.toString());
|
||||
tvWalletMnemonic.setText(walletStatus.toString());
|
||||
tvWalletViewKey.setText(walletStatus.toString());
|
||||
tvWalletSpendKey.setText(walletStatus.toString());
|
||||
}
|
||||
hideProgress();
|
||||
}
|
||||
}
|
||||
|
||||
Listener activityCallback = null;
|
||||
ProgressListener progressCallback = null;
|
||||
AcceptListener acceptCallback = null;
|
||||
ListenerWithWallet walletCallback = null;
|
||||
PasswordChangedListener passwordCallback = null;
|
||||
|
||||
public interface Listener {
|
||||
void setTitle(String title, String subtitle);
|
||||
|
||||
void setToolbarButton(int type);
|
||||
}
|
||||
|
||||
public interface ProgressListener {
|
||||
void showProgressDialog(int msgId);
|
||||
|
||||
void showLedgerProgressDialog(int mode);
|
||||
|
||||
void dismissProgressDialog();
|
||||
}
|
||||
|
||||
public interface AcceptListener {
|
||||
void onAccept(String name, String password);
|
||||
}
|
||||
|
||||
public interface ListenerWithWallet {
|
||||
Wallet getWallet();
|
||||
}
|
||||
|
||||
public interface PasswordChangedListener {
|
||||
void onPasswordChanged(String newPassword);
|
||||
|
||||
String getPassword();
|
||||
}
|
||||
|
||||
private String localPassword = null;
|
||||
|
||||
private String getPassword() {
|
||||
if (passwordCallback != null) return passwordCallback.getPassword();
|
||||
return localPassword;
|
||||
}
|
||||
|
||||
private void setPassword(String password) {
|
||||
if (passwordCallback != null) passwordCallback.onPasswordChanged(password);
|
||||
else localPassword = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
this.activityCallback = (Listener) context;
|
||||
}
|
||||
if (context instanceof ProgressListener) {
|
||||
this.progressCallback = (ProgressListener) context;
|
||||
}
|
||||
if (context instanceof AcceptListener) {
|
||||
this.acceptCallback = (AcceptListener) context;
|
||||
}
|
||||
if (context instanceof ListenerWithWallet) {
|
||||
this.walletCallback = (ListenerWithWallet) context;
|
||||
}
|
||||
if (context instanceof PasswordChangedListener) {
|
||||
this.passwordCallback = (PasswordChangedListener) context;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activityCallback.setTitle(walletName, getString(R.string.details_title));
|
||||
activityCallback.setToolbarButton(
|
||||
GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type) ? Toolbar.BUTTON_NONE : Toolbar.BUTTON_BACK);
|
||||
}
|
||||
|
||||
public void showProgress() {
|
||||
pbProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void hideProgress() {
|
||||
pbProgress.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
boolean backOk() {
|
||||
return !type.equals(GenerateReviewFragment.VIEW_TYPE_ACCEPT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
String type = getArguments().getString(REQUEST_TYPE); // intance variable <type> not set yet
|
||||
if (GenerateReviewFragment.VIEW_TYPE_ACCEPT.equals(type)) {
|
||||
inflater.inflate(R.menu.wallet_details_help_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
} else {
|
||||
inflater.inflate(R.menu.wallet_details_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
}
|
||||
|
||||
boolean changeWalletPassword(String newPassword) {
|
||||
Wallet wallet;
|
||||
boolean closeWallet;
|
||||
if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) {
|
||||
wallet = GenerateReviewFragment.this.walletCallback.getWallet();
|
||||
closeWallet = false;
|
||||
} else {
|
||||
wallet = WalletManager.getInstance().openWallet(walletPath, getPassword());
|
||||
closeWallet = true;
|
||||
}
|
||||
|
||||
boolean ok = false;
|
||||
Wallet.Status walletStatus = wallet.getStatus();
|
||||
if (walletStatus.isOk()) {
|
||||
wallet.setPassword(newPassword);
|
||||
ok = true;
|
||||
}
|
||||
if (closeWallet) wallet.close();
|
||||
return ok;
|
||||
}
|
||||
|
||||
private class AsyncChangePassword extends AsyncTask<String, Void, Boolean> {
|
||||
String newPassword;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
if (progressCallback != null)
|
||||
progressCallback.showProgressDialog(R.string.changepw_progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(String... params) {
|
||||
if (params.length != 2) return false;
|
||||
final String userPassword = params[0];
|
||||
final boolean fingerPassValid = Boolean.parseBoolean(params[1]);
|
||||
newPassword = KeyStoreHelper.getCrazyPass(getActivity(), userPassword);
|
||||
final boolean success = changeWalletPassword(newPassword);
|
||||
if (success) {
|
||||
Context ctx = getActivity();
|
||||
if (ctx != null)
|
||||
if (fingerPassValid) {
|
||||
KeyStoreHelper.saveWalletUserPass(ctx, walletName, userPassword);
|
||||
} else {
|
||||
KeyStoreHelper.removeWalletUserPass(ctx, walletName);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
if ((getActivity() == null) || getActivity().isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
if (progressCallback != null)
|
||||
progressCallback.dismissProgressDialog();
|
||||
if (result) {
|
||||
Toast.makeText(getActivity(), getString(R.string.changepw_success), Toast.LENGTH_SHORT).show();
|
||||
setPassword(newPassword);
|
||||
showDetails();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.changepw_failed), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog openDialog = null; // for preventing opening of multiple dialogs
|
||||
|
||||
public AlertDialog createChangePasswordDialog() {
|
||||
if (openDialog != null) return null; // we are already open
|
||||
LayoutInflater li = LayoutInflater.from(getActivity());
|
||||
View promptsView = li.inflate(R.layout.prompt_changepw, null);
|
||||
|
||||
AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(requireActivity());
|
||||
alertDialogBuilder.setView(promptsView);
|
||||
|
||||
final PasswordEntryView etPasswordA = promptsView.findViewById(R.id.etWalletPasswordA);
|
||||
etPasswordA.setHint(getString(R.string.prompt_changepw, walletName));
|
||||
|
||||
final TextInputLayout etPasswordB = promptsView.findViewById(R.id.etWalletPasswordB);
|
||||
etPasswordB.setHint(getString(R.string.prompt_changepwB, walletName));
|
||||
|
||||
LinearLayout llFingerprintAuth = promptsView.findViewById(R.id.llFingerprintAuth);
|
||||
final SwitchMaterial swFingerprintAllowed = (SwitchMaterial) llFingerprintAuth.getChildAt(0);
|
||||
if (FingerprintHelper.isDeviceSupported(getActivity())) {
|
||||
llFingerprintAuth.setVisibility(View.VISIBLE);
|
||||
|
||||
swFingerprintAllowed.setOnClickListener(view -> {
|
||||
if (!swFingerprintAllowed.isChecked()) return;
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
|
||||
builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn)))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
swFingerprintAllowed.setChecked(false);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
});
|
||||
|
||||
swFingerprintAllowed.setChecked(FingerprintHelper.isFingerPassValid(getActivity(), walletName));
|
||||
}
|
||||
|
||||
etPasswordA.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (etPasswordB.getError() != null) {
|
||||
etPasswordB.setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
etPasswordB.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (etPasswordB.getError() != null) {
|
||||
etPasswordB.setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
// set dialog message
|
||||
alertDialogBuilder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Helper.hideKeyboardAlways(requireActivity());
|
||||
dialog.cancel();
|
||||
openDialog = null;
|
||||
}
|
||||
});
|
||||
|
||||
openDialog = alertDialogBuilder.create();
|
||||
openDialog.setOnShowListener(dialog -> {
|
||||
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(view -> {
|
||||
String newPasswordA = etPasswordA.getEditText().getText().toString();
|
||||
String newPasswordB = etPasswordB.getEditText().getText().toString();
|
||||
if (!newPasswordA.equals(newPasswordB)) {
|
||||
etPasswordB.setError(getString(R.string.generate_bad_passwordB));
|
||||
} else {
|
||||
new AsyncChangePassword().execute(newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked()));
|
||||
Helper.hideKeyboardAlways(requireActivity());
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// accept keyboard "ok"
|
||||
etPasswordB.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
String newPasswordA = etPasswordA.getEditText().getText().toString();
|
||||
String newPasswordB = etPasswordB.getEditText().getText().toString();
|
||||
// disallow empty passwords
|
||||
if (!newPasswordA.equals(newPasswordB)) {
|
||||
etPasswordB.setError(getString(R.string.generate_bad_passwordB));
|
||||
} else {
|
||||
new AsyncChangePassword().execute(newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked()));
|
||||
Helper.hideKeyboardAlways(requireActivity());
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (Helper.preventScreenshot()) {
|
||||
openDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
return openDialog;
|
||||
}
|
||||
|
||||
private boolean isKeyValid(String key) {
|
||||
return (key != null) && (key.length() == 64)
|
||||
&& !key.equals("0000000000000000000000000000000000000000000000000000000000000000")
|
||||
&& !key.toLowerCase().equals("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
||||
// ledger implmenetation returns the spend key as f's
|
||||
}
|
||||
|
||||
private class AsyncShowSeed extends AsyncTask<String, Void, Boolean> {
|
||||
String seed;
|
||||
String offset;
|
||||
Wallet.Status walletStatus;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
offset = getSeedOffset();
|
||||
flWalletMnemonic.setAlpha(0.1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(String... params) {
|
||||
if (params.length != 1) return false;
|
||||
String walletPath = params[0];
|
||||
|
||||
Wallet wallet;
|
||||
boolean closeWallet;
|
||||
if (type.equals(GenerateReviewFragment.VIEW_TYPE_WALLET)) {
|
||||
wallet = GenerateReviewFragment.this.walletCallback.getWallet();
|
||||
closeWallet = false;
|
||||
} else {
|
||||
wallet = WalletManager.getInstance().openWallet(walletPath, getPassword());
|
||||
closeWallet = true;
|
||||
}
|
||||
walletStatus = wallet.getStatus();
|
||||
if (!walletStatus.isOk()) {
|
||||
if (closeWallet) wallet.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
seed = wallet.getSeed(offset);
|
||||
if (closeWallet) wallet.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
if (!isAdded()) return; // never mind
|
||||
if (result) {
|
||||
if (!seed.isEmpty()) {
|
||||
llMnemonic.setVisibility(View.VISIBLE);
|
||||
tvWalletMnemonic.setText(seed);
|
||||
}
|
||||
} else {
|
||||
tvWalletMnemonic.setText(walletStatus.toString());
|
||||
}
|
||||
seedOffsetInProgress = false;
|
||||
if (!getSeedOffset().equals(offset)) { // make sure we have encrypted with the correct offset
|
||||
showSeed(); // seed has changed in the meantime - recalc
|
||||
} else
|
||||
flWalletMnemonic.setAlpha(1);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,563 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
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.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
||||
import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
import com.m2049r.xmrwallet.util.NodePinger;
|
||||
import com.m2049r.xmrwallet.util.Notice;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInteractionListener,
|
||||
View.OnClickListener {
|
||||
|
||||
private WalletInfoAdapter adapter;
|
||||
|
||||
private final List<WalletManager.WalletInfo> walletList = new ArrayList<>();
|
||||
|
||||
private View tvGuntherSays;
|
||||
private ImageView ivGunther;
|
||||
private TextView tvNodeName;
|
||||
private TextView tvNodeInfo;
|
||||
private ImageButton ibNetwork;
|
||||
private CircularProgressIndicator pbNetwork;
|
||||
|
||||
private Listener activityCallback;
|
||||
|
||||
// Container Activity must implement this interface
|
||||
public interface Listener {
|
||||
File getStorageRoot();
|
||||
|
||||
boolean onWalletSelected(String wallet, boolean streetmode);
|
||||
|
||||
void onWalletDetails(String wallet);
|
||||
|
||||
void onWalletRename(String name);
|
||||
|
||||
void onWalletBackup(String name);
|
||||
|
||||
void onWalletRestore();
|
||||
|
||||
void onWalletDelete(String walletName);
|
||||
|
||||
void onWalletDeleteCache(String walletName);
|
||||
|
||||
void onAddWallet(String type);
|
||||
|
||||
void onNodePrefs();
|
||||
|
||||
void showNet();
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setTitle(String title);
|
||||
|
||||
void setNode(NodeInfo node);
|
||||
|
||||
NodeInfo getNode();
|
||||
|
||||
Set<NodeInfo> getFavouriteNodes();
|
||||
|
||||
Set<NodeInfo> getOrPopulateFavourites();
|
||||
|
||||
boolean hasLedger();
|
||||
|
||||
void runOnNetCipher(Runnable runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
this.activityCallback = (Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
Timber.d("onPause()");
|
||||
torStatus = null;
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume() %s", activityCallback.getFavouriteNodes().size());
|
||||
activityCallback.setTitle(null);
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_SETTINGS);
|
||||
activityCallback.showNet();
|
||||
showNetwork();
|
||||
//activityCallback.runOnNetCipher(this::pingSelectedNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
Timber.d("onCreateView");
|
||||
View view = inflater.inflate(R.layout.fragment_login, container, false);
|
||||
|
||||
tvGuntherSays = view.findViewById(R.id.tvGuntherSays);
|
||||
ivGunther = view.findViewById(R.id.ivGunther);
|
||||
fabScreen = view.findViewById(R.id.fabScreen);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
fabNew = view.findViewById(R.id.fabNew);
|
||||
fabView = view.findViewById(R.id.fabView);
|
||||
fabKey = view.findViewById(R.id.fabKey);
|
||||
fabSeed = view.findViewById(R.id.fabSeed);
|
||||
fabImport = view.findViewById(R.id.fabImport);
|
||||
fabLedger = view.findViewById(R.id.fabLedger);
|
||||
|
||||
fabNewL = view.findViewById(R.id.fabNewL);
|
||||
fabViewL = view.findViewById(R.id.fabViewL);
|
||||
fabKeyL = view.findViewById(R.id.fabKeyL);
|
||||
fabSeedL = view.findViewById(R.id.fabSeedL);
|
||||
fabImportL = view.findViewById(R.id.fabImportL);
|
||||
fabLedgerL = view.findViewById(R.id.fabLedgerL);
|
||||
|
||||
fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse);
|
||||
fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen);
|
||||
fab_close_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_close_screen);
|
||||
fab_open = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open);
|
||||
fab_close = AnimationUtils.loadAnimation(getContext(), R.anim.fab_close);
|
||||
rotate_forward = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_forward);
|
||||
rotate_backward = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_backward);
|
||||
fab.setOnClickListener(this);
|
||||
fabNew.setOnClickListener(this);
|
||||
fabView.setOnClickListener(this);
|
||||
fabKey.setOnClickListener(this);
|
||||
fabSeed.setOnClickListener(this);
|
||||
fabImport.setOnClickListener(this);
|
||||
fabLedger.setOnClickListener(this);
|
||||
fabScreen.setOnClickListener(this);
|
||||
|
||||
RecyclerView recyclerView = view.findViewById(R.id.list);
|
||||
registerForContextMenu(recyclerView);
|
||||
this.adapter = new WalletInfoAdapter(getActivity(), this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
ViewGroup llNotice = view.findViewById(R.id.llNotice);
|
||||
Notice.showAll(llNotice, ".*_login");
|
||||
|
||||
view.findViewById(R.id.llNode).setOnClickListener(v -> startNodePrefs());
|
||||
tvNodeName = view.findViewById(R.id.tvNodeName);
|
||||
tvNodeInfo = view.findViewById(R.id.tvInfo);
|
||||
view.findViewById(R.id.ibRenew).setOnClickListener(v -> findBestNode());
|
||||
ibNetwork = view.findViewById(R.id.ibNetwork);
|
||||
ibNetwork.setOnClickListener(v -> changeNetwork());
|
||||
ibNetwork.setEnabled(false);
|
||||
pbNetwork = view.findViewById(R.id.pbNetwork);
|
||||
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
loadList();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
// Callbacks from WalletInfoAdapter
|
||||
|
||||
// Wallet touched
|
||||
@Override
|
||||
public void onInteraction(final View view, final WalletManager.WalletInfo infoItem) {
|
||||
openWallet(infoItem.getName(), false);
|
||||
}
|
||||
|
||||
private void openWallet(String name, boolean streetmode) {
|
||||
activityCallback.onWalletSelected(name, streetmode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo listItem) {
|
||||
final int id = item.getItemId();
|
||||
if (id == R.id.action_streetmode) {
|
||||
openWallet(listItem.getName(), true);
|
||||
} else if (id == R.id.action_info) {
|
||||
showInfo(listItem.getName());
|
||||
} else if (id == R.id.action_rename) {
|
||||
activityCallback.onWalletRename(listItem.getName());
|
||||
} else if (id == R.id.action_backup) {
|
||||
activityCallback.onWalletBackup(listItem.getName());
|
||||
} else if (id == R.id.action_archive) {
|
||||
activityCallback.onWalletDelete(listItem.getName());
|
||||
} else if (id == R.id.action_deletecache) {
|
||||
activityCallback.onWalletDeleteCache(listItem.getName());
|
||||
} else {
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void loadList() {
|
||||
Timber.d("loadList()");
|
||||
WalletManager mgr = WalletManager.getInstance();
|
||||
walletList.clear();
|
||||
walletList.addAll(mgr.findWallets(activityCallback.getStorageRoot()));
|
||||
adapter.setInfos(walletList);
|
||||
|
||||
// deal with Gunther & FAB animation
|
||||
if (walletList.isEmpty()) {
|
||||
fab.startAnimation(fab_pulse);
|
||||
if (ivGunther.getDrawable() == null) {
|
||||
ivGunther.setImageResource(R.drawable.ic_emptygunther);
|
||||
tvGuntherSays.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
fab.clearAnimation();
|
||||
if (ivGunther.getDrawable() != null) {
|
||||
ivGunther.setImageDrawable(null);
|
||||
}
|
||||
tvGuntherSays.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// remove information of non-existent wallet
|
||||
Set<String> removedWallets = getActivity()
|
||||
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getAll().keySet();
|
||||
for (WalletManager.WalletInfo s : walletList) {
|
||||
removedWallets.remove(s.getName());
|
||||
}
|
||||
for (String name : removedWallets) {
|
||||
KeyStoreHelper.removeWalletUserPass(getActivity(), name);
|
||||
}
|
||||
}
|
||||
|
||||
private void showInfo(@NonNull String name) {
|
||||
activityCallback.onWalletDetails(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.list_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
private boolean isFabOpen = false;
|
||||
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabImport, fabLedger;
|
||||
private RelativeLayout fabScreen;
|
||||
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabImportL, fabLedgerL;
|
||||
private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen;
|
||||
private Animation fab_pulse;
|
||||
|
||||
public boolean isFabOpen() {
|
||||
return isFabOpen;
|
||||
}
|
||||
|
||||
public void animateFAB() {
|
||||
if (isFabOpen) { // close the fab
|
||||
fabScreen.setClickable(false);
|
||||
fabScreen.startAnimation(fab_close_screen);
|
||||
fab.startAnimation(rotate_backward);
|
||||
if (fabLedgerL.getVisibility() == View.VISIBLE) {
|
||||
fabLedgerL.startAnimation(fab_close);
|
||||
fabLedger.setClickable(false);
|
||||
} else {
|
||||
fabNewL.startAnimation(fab_close);
|
||||
fabNew.setClickable(false);
|
||||
fabViewL.startAnimation(fab_close);
|
||||
fabView.setClickable(false);
|
||||
fabKeyL.startAnimation(fab_close);
|
||||
fabKey.setClickable(false);
|
||||
fabSeedL.startAnimation(fab_close);
|
||||
fabSeed.setClickable(false);
|
||||
fabImportL.startAnimation(fab_close);
|
||||
fabImport.setClickable(false);
|
||||
}
|
||||
isFabOpen = false;
|
||||
} else { // open the fab
|
||||
fabScreen.setClickable(true);
|
||||
fabScreen.startAnimation(fab_open_screen);
|
||||
fab.startAnimation(rotate_forward);
|
||||
if (activityCallback.hasLedger()) {
|
||||
fabLedgerL.setVisibility(View.VISIBLE);
|
||||
fabNewL.setVisibility(View.GONE);
|
||||
fabViewL.setVisibility(View.GONE);
|
||||
fabKeyL.setVisibility(View.GONE);
|
||||
fabSeedL.setVisibility(View.GONE);
|
||||
fabImportL.setVisibility(View.GONE);
|
||||
|
||||
fabLedgerL.startAnimation(fab_open);
|
||||
fabLedger.setClickable(true);
|
||||
} else {
|
||||
fabLedgerL.setVisibility(View.GONE);
|
||||
fabNewL.setVisibility(View.VISIBLE);
|
||||
fabViewL.setVisibility(View.VISIBLE);
|
||||
fabKeyL.setVisibility(View.VISIBLE);
|
||||
fabSeedL.setVisibility(View.VISIBLE);
|
||||
fabImportL.setVisibility(View.VISIBLE);
|
||||
|
||||
fabNewL.startAnimation(fab_open);
|
||||
fabNew.setClickable(true);
|
||||
fabViewL.startAnimation(fab_open);
|
||||
fabView.setClickable(true);
|
||||
fabKeyL.startAnimation(fab_open);
|
||||
fabKey.setClickable(true);
|
||||
fabSeedL.startAnimation(fab_open);
|
||||
fabSeed.setClickable(true);
|
||||
fabImportL.startAnimation(fab_open);
|
||||
fabImport.setClickable(true);
|
||||
}
|
||||
isFabOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final int id = v.getId();
|
||||
Timber.d("onClick %d/%d", id, R.id.fabLedger);
|
||||
if (id == R.id.fab) {
|
||||
animateFAB();
|
||||
} else if (id == R.id.fabNew) {
|
||||
fabScreen.setVisibility(View.INVISIBLE);
|
||||
isFabOpen = false;
|
||||
activityCallback.onAddWallet(GenerateFragment.TYPE_NEW);
|
||||
} else if (id == R.id.fabView) {
|
||||
animateFAB();
|
||||
activityCallback.onAddWallet(GenerateFragment.TYPE_VIEWONLY);
|
||||
} else if (id == R.id.fabKey) {
|
||||
animateFAB();
|
||||
activityCallback.onAddWallet(GenerateFragment.TYPE_KEY);
|
||||
} else if (id == R.id.fabSeed) {
|
||||
animateFAB();
|
||||
activityCallback.onAddWallet(GenerateFragment.TYPE_SEED);
|
||||
} else if (id == R.id.fabImport) {
|
||||
animateFAB();
|
||||
activityCallback.onWalletRestore();
|
||||
} else if (id == R.id.fabLedger) {
|
||||
Timber.d("FAB_LEDGER");
|
||||
animateFAB();
|
||||
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
|
||||
} else if (id == R.id.fabScreen) {
|
||||
animateFAB();
|
||||
}
|
||||
}
|
||||
|
||||
public void findBestNode() {
|
||||
new AsyncFindBestNode().execute(AsyncFindBestNode.FIND_BEST);
|
||||
}
|
||||
|
||||
public void pingSelectedNode() {
|
||||
new AsyncFindBestNode().execute(AsyncFindBestNode.PING_SELECTED);
|
||||
}
|
||||
|
||||
private NodeInfo autoselect(Set<NodeInfo> nodes) {
|
||||
if (nodes.isEmpty()) return null;
|
||||
NodePinger.execute(nodes, null);
|
||||
List<NodeInfo> nodeList = new ArrayList<>(nodes);
|
||||
Collections.sort(nodeList, NodeInfo.BestNodeComparator);
|
||||
return nodeList.get(0);
|
||||
}
|
||||
|
||||
private void setSubtext(String status) {
|
||||
final Context ctx = getContext();
|
||||
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),
|
||||
status, ""));
|
||||
tvNodeInfo.setText(text);
|
||||
}
|
||||
|
||||
private class AsyncFindBestNode extends AsyncTask<Integer, Void, NodeInfo> {
|
||||
final static int PING_SELECTED = 0;
|
||||
final static int FIND_BEST = 1;
|
||||
|
||||
private boolean netState;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
tvNodeName.setVisibility(View.GONE);
|
||||
pbNetwork.setVisibility(View.VISIBLE);
|
||||
netState = ibNetwork.isClickable();
|
||||
ibNetwork.setClickable(false);
|
||||
setSubtext(getString(R.string.node_waiting));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeInfo doInBackground(Integer... params) {
|
||||
Set<NodeInfo> favourites = activityCallback.getOrPopulateFavourites();
|
||||
NodeInfo selectedNode;
|
||||
if (params[0] == FIND_BEST) {
|
||||
selectedNode = autoselect(favourites);
|
||||
} else if (params[0] == PING_SELECTED) {
|
||||
selectedNode = activityCallback.getNode();
|
||||
if (!activityCallback.getFavouriteNodes().contains(selectedNode))
|
||||
selectedNode = null; // it's not in the favourites (any longer)
|
||||
if (selectedNode == null)
|
||||
for (NodeInfo node : favourites) {
|
||||
if (node.isSelected()) {
|
||||
selectedNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectedNode == null) { // autoselect
|
||||
selectedNode = autoselect(favourites);
|
||||
} else {
|
||||
selectedNode.testRpcService();
|
||||
}
|
||||
} else throw new IllegalStateException();
|
||||
if ((selectedNode != null) && selectedNode.isValid()) {
|
||||
activityCallback.setNode(selectedNode);
|
||||
return selectedNode;
|
||||
} else {
|
||||
activityCallback.setNode(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(NodeInfo result) {
|
||||
if (!isAdded()) return;
|
||||
tvNodeName.setVisibility(View.VISIBLE);
|
||||
pbNetwork.setVisibility(View.INVISIBLE);
|
||||
ibNetwork.setClickable(netState);
|
||||
if (result != null) {
|
||||
Timber.d("found a good node %s", result.toString());
|
||||
showNode(result);
|
||||
} else {
|
||||
tvNodeName.setText(getResources().getText(R.string.node_create_hint));
|
||||
tvNodeName.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
tvNodeInfo.setText(null);
|
||||
tvNodeInfo.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(NodeInfo result) { //TODO: cancel this on exit from fragment
|
||||
Timber.d("cancelled with %s", result);
|
||||
}
|
||||
}
|
||||
|
||||
private void showNode(NodeInfo nodeInfo) {
|
||||
tvNodeName.setText(nodeInfo.getName());
|
||||
nodeInfo.showInfo(tvNodeInfo);
|
||||
tvNodeInfo.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void startNodePrefs() {
|
||||
activityCallback.onNodePrefs();
|
||||
}
|
||||
|
||||
// Network (Tor) stuff
|
||||
|
||||
private void changeNetwork() {
|
||||
Timber.d("S: %s", NetCipherHelper.getStatus());
|
||||
final NetCipherHelper.Status status = NetCipherHelper.getStatus();
|
||||
if (status == NetCipherHelper.Status.NOT_INSTALLED) {
|
||||
HelpFragment.display(requireActivity().getSupportFragmentManager(), R.string.help_tor);
|
||||
} else if (status == NetCipherHelper.Status.NOT_ENABLED) {
|
||||
Toast.makeText(getActivity(), getString(R.string.tor_enable_background), Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
pbNetwork.setVisibility(View.VISIBLE);
|
||||
ibNetwork.setEnabled(false);
|
||||
NetCipherHelper.getInstance().toggle();
|
||||
}
|
||||
}
|
||||
|
||||
private NetCipherHelper.Status torStatus = null;
|
||||
|
||||
void showNetwork() {
|
||||
final NetCipherHelper.Status status = NetCipherHelper.getStatus();
|
||||
Timber.d("SHOW %s", status);
|
||||
if (status == torStatus) return;
|
||||
torStatus = status;
|
||||
switch (status) {
|
||||
case ENABLED:
|
||||
ibNetwork.setImageResource(R.drawable.ic_network_tor_on);
|
||||
ibNetwork.setEnabled(true);
|
||||
ibNetwork.setClickable(true);
|
||||
pbNetwork.setVisibility(View.INVISIBLE);
|
||||
break;
|
||||
case NOT_ENABLED:
|
||||
case DISABLED:
|
||||
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
|
||||
ibNetwork.setEnabled(true);
|
||||
ibNetwork.setClickable(true);
|
||||
pbNetwork.setVisibility(View.INVISIBLE);
|
||||
break;
|
||||
case STARTING:
|
||||
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
|
||||
ibNetwork.setEnabled(false);
|
||||
pbNetwork.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case STOPPING:
|
||||
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
|
||||
ibNetwork.setEnabled(false);
|
||||
pbNetwork.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case NOT_INSTALLED:
|
||||
ibNetwork.setEnabled(true);
|
||||
ibNetwork.setClickable(true);
|
||||
pbNetwork.setVisibility(View.INVISIBLE);
|
||||
ibNetwork.setImageResource(R.drawable.ic_network_clearnet);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
activityCallback.runOnNetCipher(this::pingSelectedNode);
|
||||
}
|
||||
}
|
@ -1,39 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2020 EarlOfEgo, 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;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.MoneroHandlerThread;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import com.m2049r.xmrwallet.onboarding.OnBoardingActivity;
|
||||
import com.m2049r.xmrwallet.onboarding.OnBoardingManager;
|
||||
public class MainActivity extends AppCompatActivity implements MoneroHandlerThread.Listener {
|
||||
private final MutableLiveData<String> _address = new MutableLiveData<>("");
|
||||
public LiveData<String> address = _address;
|
||||
private final MutableLiveData<Long> _balance = new MutableLiveData<>(0L);
|
||||
public LiveData<Long> balance = _balance;
|
||||
private final MutableLiveData<List<TransactionInfo>> _history = new MutableLiveData<>();
|
||||
public LiveData<List<TransactionInfo>> history = _history;
|
||||
|
||||
private MoneroHandlerThread thread = null;
|
||||
private TxService txService = null;
|
||||
|
||||
public class MainActivity extends BaseActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (OnBoardingManager.shouldShowOnBoarding(getApplicationContext())) {
|
||||
startActivity(new Intent(this, OnBoardingActivity.class));
|
||||
} else {
|
||||
startActivity(new Intent(this, LoginActivity.class));
|
||||
}
|
||||
finish();
|
||||
setContentView(R.layout.activity_main);
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroHandlerThread getThread() {
|
||||
return thread;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
File walletFile = new File(getApplicationInfo().dataDir, "xmr_wallet");
|
||||
Wallet wallet = null;
|
||||
if(walletFile.exists()) {
|
||||
wallet = WalletManager.getInstance().openWallet(walletFile.getAbsolutePath(), "");
|
||||
} else {
|
||||
wallet = WalletManager.getInstance().createWallet(walletFile, "", "English", 0);
|
||||
}
|
||||
WalletManager.getInstance().setProxy("127.0.0.1:9050");
|
||||
thread = new MoneroHandlerThread("WalletService", wallet, this);
|
||||
thread.start();
|
||||
this.txService = new TxService(this, thread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
WalletManager walletManager = WalletManager.getInstance();
|
||||
Wallet wallet = walletManager.getWallet();
|
||||
if(wallet != null) {
|
||||
String address = wallet.getLastSubaddress(0);
|
||||
_history.postValue(wallet.getHistory().getAll());
|
||||
_balance.postValue(wallet.getBalance());
|
||||
_address.postValue(address);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,589 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.levin.scanner.Dispatcher;
|
||||
import com.m2049r.xmrwallet.data.DefaultNodes;
|
||||
import com.m2049r.xmrwallet.data.Node;
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
import com.m2049r.xmrwallet.layout.NodeInfoAdapter;
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.NodePinger;
|
||||
import com.m2049r.xmrwallet.util.Notice;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class NodeFragment extends Fragment
|
||||
implements NodeInfoAdapter.OnInteractionListener, View.OnClickListener {
|
||||
|
||||
static private int NODES_TO_FIND = 10;
|
||||
|
||||
static private NumberFormat FORMATTER = NumberFormat.getInstance();
|
||||
|
||||
private SwipeRefreshLayout pullToRefresh;
|
||||
private TextView tvPull;
|
||||
private View fab;
|
||||
|
||||
private Set<NodeInfo> nodeList = new HashSet<>();
|
||||
|
||||
private NodeInfoAdapter nodesAdapter;
|
||||
|
||||
private Listener activityCallback;
|
||||
|
||||
public interface Listener {
|
||||
File getStorageRoot();
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setSubtitle(String title);
|
||||
|
||||
Set<NodeInfo> getFavouriteNodes();
|
||||
|
||||
Set<NodeInfo> getOrPopulateFavourites();
|
||||
|
||||
void setFavouriteNodes(Collection<NodeInfo> favouriteNodes);
|
||||
|
||||
void setNode(NodeInfo node);
|
||||
}
|
||||
|
||||
void filterFavourites() {
|
||||
for (Iterator<NodeInfo> iter = nodeList.iterator(); iter.hasNext(); ) {
|
||||
Node node = iter.next();
|
||||
if (!node.isFavourite()) iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
this.activityCallback = (Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
Timber.d("onPause() %d", nodeList.size());
|
||||
if (asyncFindNodes != null)
|
||||
asyncFindNodes.cancel(true);
|
||||
if (activityCallback != null)
|
||||
activityCallback.setFavouriteNodes(nodeList);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activityCallback.setSubtitle(getString(R.string.label_nodes));
|
||||
updateRefreshElements();
|
||||
}
|
||||
|
||||
boolean isRefreshing() {
|
||||
return asyncFindNodes != null;
|
||||
}
|
||||
|
||||
void updateRefreshElements() {
|
||||
if (isRefreshing()) {
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
|
||||
fab.setVisibility(View.GONE);
|
||||
} else {
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
Timber.d("onCreateView");
|
||||
View view = inflater.inflate(R.layout.fragment_node, container, false);
|
||||
|
||||
fab = view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this);
|
||||
|
||||
RecyclerView recyclerView = view.findViewById(R.id.list);
|
||||
nodesAdapter = new NodeInfoAdapter(getActivity(), this);
|
||||
recyclerView.setAdapter(nodesAdapter);
|
||||
|
||||
tvPull = view.findViewById(R.id.tvPull);
|
||||
|
||||
pullToRefresh = view.findViewById(R.id.pullToRefresh);
|
||||
pullToRefresh.setOnRefreshListener(() -> {
|
||||
if (WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) {
|
||||
refresh(AsyncFindNodes.SCAN);
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.node_wrong_net), Toast.LENGTH_LONG).show();
|
||||
pullToRefresh.setRefreshing(false);
|
||||
}
|
||||
});
|
||||
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
nodeList = new HashSet<>(activityCallback.getFavouriteNodes());
|
||||
nodesAdapter.setNodes(nodeList);
|
||||
|
||||
ViewGroup llNotice = view.findViewById(R.id.llNotice);
|
||||
Notice.showAll(llNotice, ".*_nodes");
|
||||
|
||||
refresh(AsyncFindNodes.PING); // start connection tests
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private AsyncFindNodes asyncFindNodes = null;
|
||||
|
||||
private boolean refresh(int type) {
|
||||
if (asyncFindNodes != null) return false; // ignore refresh request as one is ongoing
|
||||
asyncFindNodes = new AsyncFindNodes();
|
||||
updateRefreshElements();
|
||||
asyncFindNodes.execute(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.node_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
// Callbacks from NodeInfoAdapter
|
||||
@Override
|
||||
public void onInteraction(final View view, final NodeInfo nodeItem) {
|
||||
Timber.d("onInteraction");
|
||||
if (!nodeItem.isFavourite()) {
|
||||
nodeItem.setFavourite(true);
|
||||
activityCallback.setFavouriteNodes(nodeList);
|
||||
}
|
||||
AsyncTask.execute(() -> {
|
||||
activityCallback.setNode(nodeItem); // this marks it as selected & saves it as well
|
||||
nodeItem.setSelecting(false);
|
||||
try {
|
||||
requireActivity().runOnUiThread(() -> nodesAdapter.allowClick(true));
|
||||
} catch (IllegalStateException ex) {
|
||||
// it's ok
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// open up edit dialog
|
||||
@Override
|
||||
public boolean onLongInteraction(final View view, final NodeInfo nodeItem) {
|
||||
Timber.d("onLongInteraction");
|
||||
EditDialog diag = createEditDialog(nodeItem);
|
||||
if (diag != null) {
|
||||
diag.show();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int id = v.getId();
|
||||
if (id == R.id.fab) {
|
||||
EditDialog diag = createEditDialog(null);
|
||||
if (diag != null) {
|
||||
diag.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncFindNodes extends AsyncTask<Integer, NodeInfo, Boolean>
|
||||
implements NodePinger.Listener {
|
||||
final static int SCAN = 0;
|
||||
final static int RESTORE_DEFAULTS = 1;
|
||||
final static int PING = 2;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
filterFavourites();
|
||||
nodesAdapter.setNodes(null);
|
||||
nodesAdapter.allowClick(false);
|
||||
tvPull.setText(getString(R.string.node_scanning));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Integer... params) {
|
||||
if (params[0] == RESTORE_DEFAULTS) { // true = restore defaults
|
||||
for (DefaultNodes node : DefaultNodes.values()) {
|
||||
NodeInfo nodeInfo = NodeInfo.fromString(node.getUri());
|
||||
if (nodeInfo != null) {
|
||||
nodeInfo.setFavourite(true);
|
||||
nodeList.add(nodeInfo);
|
||||
}
|
||||
}
|
||||
NodePinger.execute(nodeList, this);
|
||||
return true;
|
||||
} else if (params[0] == PING) {
|
||||
NodePinger.execute(nodeList, this);
|
||||
return true;
|
||||
} else if (params[0] == SCAN) {
|
||||
// otherwise scan the network
|
||||
Timber.d("scanning");
|
||||
Set<NodeInfo> seedList = new HashSet<>();
|
||||
seedList.addAll(nodeList);
|
||||
nodeList.clear();
|
||||
Timber.d("seed %d", seedList.size());
|
||||
Dispatcher d = new Dispatcher(info -> publishProgress(info));
|
||||
d.seedPeers(seedList);
|
||||
d.awaitTermination(NODES_TO_FIND);
|
||||
|
||||
// we didn't find enough because we didn't ask around enough? ask more!
|
||||
if ((d.getRpcNodes().size() < NODES_TO_FIND) &&
|
||||
(d.getPeerCount() < NODES_TO_FIND + seedList.size())) {
|
||||
// try again
|
||||
publishProgress((NodeInfo[]) null);
|
||||
d = new Dispatcher(new Dispatcher.Listener() {
|
||||
@Override
|
||||
public void onGet(NodeInfo info) {
|
||||
publishProgress(info);
|
||||
}
|
||||
});
|
||||
// also seed with monero seed nodes (see p2p/net_node.inl:410 in monero src)
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("107.152.130.98", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("212.83.175.67", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("5.9.100.248", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("163.172.182.165", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("161.67.132.39", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("198.74.231.92", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("195.154.123.123", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("212.83.172.165", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("192.110.160.146", 18080)));
|
||||
d.seedPeers(seedList);
|
||||
d.awaitTermination(NODES_TO_FIND);
|
||||
}
|
||||
// final (filtered) result
|
||||
nodeList.addAll(d.getRpcNodes());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(NodeInfo... values) {
|
||||
Timber.d("onProgressUpdate");
|
||||
if (!isCancelled())
|
||||
if (values != null)
|
||||
nodesAdapter.addNode(values[0]);
|
||||
else
|
||||
nodesAdapter.setNodes(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
Timber.d("done scanning");
|
||||
complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(Boolean result) {
|
||||
Timber.d("cancelled scanning");
|
||||
complete();
|
||||
}
|
||||
|
||||
private void complete() {
|
||||
asyncFindNodes = null;
|
||||
if (!isAdded()) return;
|
||||
//if (isCancelled()) return;
|
||||
tvPull.setText(getString(R.string.node_pull_hint));
|
||||
pullToRefresh.setRefreshing(false);
|
||||
nodesAdapter.setNodes(nodeList);
|
||||
nodesAdapter.allowClick(true);
|
||||
updateRefreshElements();
|
||||
}
|
||||
|
||||
public void publish(NodeInfo nodeInfo) {
|
||||
publishProgress(nodeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
Timber.d("detached");
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
private EditDialog editDialog = null; // for preventing opening of multiple dialogs
|
||||
|
||||
private EditDialog createEditDialog(final NodeInfo nodeInfo) {
|
||||
if (editDialog != null) return null; // we are already open
|
||||
editDialog = new EditDialog(nodeInfo);
|
||||
return editDialog;
|
||||
}
|
||||
|
||||
class EditDialog {
|
||||
final NodeInfo nodeInfo;
|
||||
final NodeInfo nodeBackup;
|
||||
|
||||
private boolean applyChanges() {
|
||||
nodeInfo.clear();
|
||||
showTestResult();
|
||||
|
||||
final String portString = etNodePort.getEditText().getText().toString().trim();
|
||||
int port;
|
||||
if (portString.isEmpty()) {
|
||||
port = Node.getDefaultRpcPort();
|
||||
} else {
|
||||
try {
|
||||
port = Integer.parseInt(portString);
|
||||
} catch (NumberFormatException ex) {
|
||||
etNodePort.setError(getString(R.string.node_port_numeric));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
etNodePort.setError(null);
|
||||
if ((port <= 0) || (port > 65535)) {
|
||||
etNodePort.setError(getString(R.string.node_port_range));
|
||||
return false;
|
||||
}
|
||||
|
||||
final String host = etNodeHost.getEditText().getText().toString().trim();
|
||||
if (host.isEmpty()) {
|
||||
etNodeHost.setError(getString(R.string.node_host_empty));
|
||||
return false;
|
||||
}
|
||||
final boolean setHostSuccess = Helper.runWithNetwork(() -> {
|
||||
try {
|
||||
nodeInfo.setHost(host);
|
||||
return true;
|
||||
} catch (UnknownHostException ex) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!setHostSuccess) {
|
||||
etNodeHost.setError(getString(R.string.node_host_unresolved));
|
||||
return false;
|
||||
}
|
||||
etNodeHost.setError(null);
|
||||
nodeInfo.setRpcPort(port);
|
||||
// setName() may trigger reverse DNS
|
||||
Helper.runWithNetwork(new Helper.Action() {
|
||||
@Override
|
||||
public boolean run() {
|
||||
nodeInfo.setName(etNodeName.getEditText().getText().toString().trim());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
nodeInfo.setUsername(etNodeUser.getEditText().getText().toString().trim());
|
||||
nodeInfo.setPassword(etNodePass.getEditText().getText().toString()); // no trim for pw
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean shutdown = false;
|
||||
|
||||
private void apply() {
|
||||
if (applyChanges()) {
|
||||
closeDialog();
|
||||
if (nodeBackup == null) { // this is a (FAB) new node
|
||||
nodeInfo.setFavourite(true);
|
||||
nodeList.add(nodeInfo);
|
||||
}
|
||||
shutdown = true;
|
||||
new AsyncTestNode().execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDialog() {
|
||||
if (editDialog == null) throw new IllegalStateException();
|
||||
Helper.hideKeyboardAlways(getActivity());
|
||||
editDialog.dismiss();
|
||||
editDialog = null;
|
||||
NodeFragment.this.editDialog = null;
|
||||
}
|
||||
|
||||
private void show() {
|
||||
editDialog.show();
|
||||
}
|
||||
|
||||
private void test() {
|
||||
if (applyChanges())
|
||||
new AsyncTestNode().execute();
|
||||
}
|
||||
|
||||
private void showKeyboard() {
|
||||
Helper.showKeyboard(editDialog);
|
||||
}
|
||||
|
||||
AlertDialog editDialog = null;
|
||||
|
||||
TextInputLayout etNodeName;
|
||||
TextInputLayout etNodeHost;
|
||||
TextInputLayout etNodePort;
|
||||
TextInputLayout etNodeUser;
|
||||
TextInputLayout etNodePass;
|
||||
TextView tvResult;
|
||||
|
||||
void showTestResult() {
|
||||
if (nodeInfo.isSuccessful()) {
|
||||
tvResult.setText(getString(R.string.node_result,
|
||||
FORMATTER.format(nodeInfo.getHeight()), nodeInfo.getMajorVersion(),
|
||||
nodeInfo.getResponseTime(), nodeInfo.getHostAddress()));
|
||||
} else {
|
||||
tvResult.setText(NodeInfoAdapter.getResponseErrorText(getActivity(), nodeInfo.getResponseCode()));
|
||||
}
|
||||
}
|
||||
|
||||
EditDialog(final NodeInfo nodeInfo) {
|
||||
AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(getActivity());
|
||||
LayoutInflater li = LayoutInflater.from(alertDialogBuilder.getContext());
|
||||
View promptsView = li.inflate(R.layout.prompt_editnode, null);
|
||||
alertDialogBuilder.setView(promptsView);
|
||||
|
||||
etNodeName = promptsView.findViewById(R.id.etNodeName);
|
||||
etNodeHost = promptsView.findViewById(R.id.etNodeHost);
|
||||
etNodePort = promptsView.findViewById(R.id.etNodePort);
|
||||
etNodeUser = promptsView.findViewById(R.id.etNodeUser);
|
||||
etNodePass = promptsView.findViewById(R.id.etNodePass);
|
||||
tvResult = promptsView.findViewById(R.id.tvResult);
|
||||
|
||||
if (nodeInfo != null) {
|
||||
this.nodeInfo = nodeInfo;
|
||||
nodeBackup = new NodeInfo(nodeInfo);
|
||||
etNodeName.getEditText().setText(nodeInfo.getName());
|
||||
etNodeHost.getEditText().setText(nodeInfo.getHost());
|
||||
etNodePort.getEditText().setText(Integer.toString(nodeInfo.getRpcPort()));
|
||||
etNodeUser.getEditText().setText(nodeInfo.getUsername());
|
||||
etNodePass.getEditText().setText(nodeInfo.getPassword());
|
||||
showTestResult();
|
||||
} else {
|
||||
this.nodeInfo = new NodeInfo();
|
||||
nodeBackup = null;
|
||||
}
|
||||
|
||||
// set dialog message
|
||||
alertDialogBuilder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNeutralButton(getString(R.string.label_test), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel),
|
||||
(dialog, id) -> {
|
||||
closeDialog();
|
||||
nodesAdapter.setNodes(); // to refresh test results
|
||||
});
|
||||
|
||||
editDialog = alertDialogBuilder.create();
|
||||
// these need to be here, since we don't always close the dialog
|
||||
editDialog.setOnShowListener(new DialogInterface.OnShowListener() {
|
||||
@Override
|
||||
public void onShow(final DialogInterface dialog) {
|
||||
Button testButton = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
testButton.setOnClickListener(view -> test());
|
||||
|
||||
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(view -> apply());
|
||||
}
|
||||
});
|
||||
|
||||
if (Helper.preventScreenshot()) {
|
||||
editDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
etNodePass.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
editDialog.getButton(DialogInterface.BUTTON_NEUTRAL).requestFocus();
|
||||
test();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private class AsyncTestNode extends AsyncTask<Void, Void, Boolean> {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
tvResult.setText(getString(R.string.node_testing, nodeInfo.getHostAddress()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
nodeInfo.testRpcService();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (editDialog != null) {
|
||||
showTestResult();
|
||||
}
|
||||
if (shutdown) {
|
||||
if (nodeBackup == null) {
|
||||
nodesAdapter.addNode(nodeInfo);
|
||||
} else {
|
||||
nodesAdapter.setNodes();
|
||||
}
|
||||
nodesAdapter.notifyItemChanged(nodeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void restoreDefaultNodes() {
|
||||
if (WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) {
|
||||
if (!refresh(AsyncFindNodes.RESTORE_DEFAULTS)) {
|
||||
Toast.makeText(getActivity(), getString(R.string.toast_default_nodes), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.node_wrong_net), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
public interface OnBackPressedListener {
|
||||
boolean onBackPressed();
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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;
|
||||
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
|
||||
public interface OnBlockUpdateListener {
|
||||
void onBlockUpdate(final Wallet wallet);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
|
||||
public interface OnUriScannedListener {
|
||||
boolean onUriScanned(BarcodeData barcodeData);
|
||||
}
|
@ -1,469 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.net.Uri;
|
||||
import android.nfc.NfcManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
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;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.android.material.transition.MaterialContainerTransform;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.data.Subaddress;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.ExchangeView;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ReceiveFragment extends Fragment {
|
||||
|
||||
private ProgressBar pbProgress;
|
||||
private TextView tvAddress;
|
||||
private TextInputLayout etNotes;
|
||||
private ExchangeView evAmount;
|
||||
private TextView tvQrCode;
|
||||
private ImageView ivQrCode;
|
||||
private ImageView ivQrCodeFull;
|
||||
private EditText etDummy;
|
||||
private ImageButton bCopyAddress;
|
||||
private MenuItem shareItem;
|
||||
|
||||
private Wallet wallet = null;
|
||||
private boolean isMyWallet = false;
|
||||
|
||||
public interface Listener {
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setTitle(String title);
|
||||
|
||||
void setSubtitle(String subtitle);
|
||||
|
||||
void showSubaddresses(boolean managerMode);
|
||||
|
||||
Subaddress getSelectedSubaddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_receive, container, false);
|
||||
|
||||
pbProgress = view.findViewById(R.id.pbProgress);
|
||||
tvAddress = view.findViewById(R.id.tvAddress);
|
||||
etNotes = view.findViewById(R.id.etNotes);
|
||||
evAmount = view.findViewById(R.id.evAmount);
|
||||
ivQrCode = view.findViewById(R.id.qrCode);
|
||||
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());
|
||||
|
||||
evAmount.setOnNewAmountListener(xmr -> {
|
||||
Timber.d("new amount = %s", xmr);
|
||||
generateQr();
|
||||
if (shareRequested && (xmr != null)) share();
|
||||
});
|
||||
|
||||
evAmount.setOnFailedExchangeListener(() -> {
|
||||
if (isAdded()) {
|
||||
clearQR();
|
||||
Toast.makeText(getActivity(), getString(R.string.message_exchange_failed), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
final EditText notesEdit = etNotes.getEditText();
|
||||
notesEdit.setRawInputType(InputType.TYPE_CLASS_TEXT);
|
||||
notesEdit.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
generateQr();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
notesEdit.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
clearQR();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
tvAddress.setOnClickListener(v -> {
|
||||
listenerCallback.showSubaddresses(false);
|
||||
});
|
||||
|
||||
view.findViewById(R.id.cvQrCode).setOnClickListener(v -> {
|
||||
Helper.hideKeyboard(getActivity());
|
||||
etDummy.requestFocus();
|
||||
if (qrValid) {
|
||||
ivQrCodeFull.setImageBitmap(((BitmapDrawable) ivQrCode.getDrawable()).getBitmap());
|
||||
ivQrCodeFull.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
evAmount.doExchange();
|
||||
}
|
||||
});
|
||||
|
||||
ivQrCodeFull.setOnClickListener(v -> {
|
||||
ivQrCodeFull.setImageBitmap(null);
|
||||
ivQrCodeFull.setVisibility(View.GONE);
|
||||
});
|
||||
|
||||
showProgress();
|
||||
clearQR();
|
||||
|
||||
if (getActivity() instanceof GenerateReviewFragment.ListenerWithWallet) {
|
||||
wallet = ((GenerateReviewFragment.ListenerWithWallet) getActivity()).getWallet();
|
||||
show();
|
||||
} else {
|
||||
throw new IllegalStateException("no wallet info");
|
||||
}
|
||||
|
||||
View tvNfc = view.findViewById(R.id.tvNfc);
|
||||
NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE);
|
||||
if ((manager != null) && (manager.getDefaultAdapter() != null))
|
||||
tvNfc.setVisibility(View.VISIBLE);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
final MaterialContainerTransform transform = new MaterialContainerTransform();
|
||||
transform.setDrawingViewId(R.id.fragment_container);
|
||||
transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration));
|
||||
transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground));
|
||||
setSharedElementEnterTransition(transform);
|
||||
}
|
||||
|
||||
private boolean shareRequested = false;
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.receive_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
shareItem = menu.findItem(R.id.menu_item_share);
|
||||
shareItem.setOnMenuItemClickListener(item -> {
|
||||
if (shareRequested) return true;
|
||||
shareRequested = true;
|
||||
if (!qrValid) {
|
||||
evAmount.doExchange();
|
||||
} else {
|
||||
share();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void share() {
|
||||
shareRequested = false;
|
||||
if (saveQrCode()) {
|
||||
final Intent sendIntent = getSendIntent();
|
||||
if (sendIntent != null)
|
||||
startActivity(Intent.createChooser(sendIntent, null));
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.message_qr_failed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean saveQrCode() {
|
||||
if (!qrValid) throw new IllegalStateException("trying to save null qr code!");
|
||||
|
||||
File cachePath = new File(getActivity().getCacheDir(), "images");
|
||||
if (!cachePath.exists())
|
||||
if (!cachePath.mkdirs()) throw new IllegalStateException("cannot create images folder");
|
||||
File png = new File(cachePath, "QR.png");
|
||||
try {
|
||||
FileOutputStream stream = new FileOutputStream(png);
|
||||
Bitmap qrBitmap = ((BitmapDrawable) ivQrCode.getDrawable()).getBitmap();
|
||||
qrBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
stream.close();
|
||||
return true;
|
||||
} catch (IOException ex) {
|
||||
Timber.e(ex);
|
||||
// make sure we don't share an old qr code
|
||||
if (!png.delete()) throw new IllegalStateException("cannot delete old qr code");
|
||||
// if we manage to delete it, the URI points to nothing and the user gets a toast with the error
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Intent getSendIntent() {
|
||||
File imagePath = new File(requireActivity().getCacheDir(), "images");
|
||||
File png = new File(imagePath, "QR.png");
|
||||
Uri contentUri = FileProvider.getUriForFile(requireActivity(), BuildConfig.APPLICATION_ID + ".fileprovider", png);
|
||||
if (contentUri != null) {
|
||||
Intent shareIntent = new Intent();
|
||||
shareIntent.setAction(Intent.ACTION_SEND);
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // temp permission for receiving app to read this file
|
||||
shareIntent.setTypeAndNormalize("image/png");
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
|
||||
if (bcData != null)
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, bcData.getUriString());
|
||||
return shareIntent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void copyAddress() {
|
||||
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_address), subaddress.getAddress());
|
||||
Toast.makeText(getActivity(), getString(R.string.message_copy_address), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private boolean qrValid = false;
|
||||
|
||||
void clearQR() {
|
||||
if (qrValid) {
|
||||
ivQrCode.setImageBitmap(null);
|
||||
qrValid = false;
|
||||
if (isLoaded)
|
||||
tvQrCode.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
void setQR(Bitmap qr) {
|
||||
ivQrCode.setImageBitmap(qr);
|
||||
qrValid = true;
|
||||
tvQrCode.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
listenerCallback.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
if (wallet != null) {
|
||||
listenerCallback.setTitle(wallet.getName());
|
||||
listenerCallback.setSubtitle(wallet.getAccountLabel());
|
||||
setNewSubaddress();
|
||||
} else {
|
||||
listenerCallback.setSubtitle(getString(R.string.status_wallet_loading));
|
||||
clearQR();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLoaded = false;
|
||||
|
||||
private void show() {
|
||||
Timber.d("name=%s", wallet.getName());
|
||||
isLoaded = true;
|
||||
hideProgress();
|
||||
}
|
||||
|
||||
public BarcodeData getBarcodeData() {
|
||||
if (qrValid)
|
||||
return bcData;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
private BarcodeData bcData = null;
|
||||
|
||||
private void generateQr() {
|
||||
Timber.d("GENQR");
|
||||
String address = subaddress.getAddress();
|
||||
String notes = etNotes.getEditText().getText().toString();
|
||||
String xmrAmount = evAmount.getAmount();
|
||||
Timber.d("%s/%s/%s", xmrAmount, notes, address);
|
||||
if ((xmrAmount == null) || !Wallet.isAddressValid(address)) {
|
||||
clearQR();
|
||||
Timber.d("CLEARQR");
|
||||
return;
|
||||
}
|
||||
bcData = new BarcodeData(Crypto.XMR, address, notes, xmrAmount);
|
||||
int size = Math.max(ivQrCode.getWidth(), ivQrCode.getHeight());
|
||||
Bitmap qr = generate(bcData.getUriString(), size, size);
|
||||
if (qr != null) {
|
||||
setQR(qr);
|
||||
Timber.d("SETQR");
|
||||
etDummy.requestFocus();
|
||||
Helper.hideKeyboard(getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap generate(String text, int width, int height) {
|
||||
if ((width <= 0) || (height <= 0)) return null;
|
||||
Map<EncodeHintType, Object> hints = new HashMap<>();
|
||||
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
|
||||
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
|
||||
try {
|
||||
BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
|
||||
int[] pixels = new int[width * height];
|
||||
for (int i = 0; i < height; i++) {
|
||||
for (int j = 0; j < width; j++) {
|
||||
if (bitMatrix.get(j, i)) {
|
||||
pixels[i * width + j] = 0x00000000;
|
||||
} else {
|
||||
pixels[i * height + j] = 0xffffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.RGB_565);
|
||||
bitmap = addLogo(bitmap);
|
||||
return bitmap;
|
||||
} catch (WriterException ex) {
|
||||
Timber.e(ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Bitmap addLogo(Bitmap qrBitmap) {
|
||||
// addume logo & qrcode are both square
|
||||
Bitmap logo = getMoneroLogo();
|
||||
final int qrSize = qrBitmap.getWidth();
|
||||
final int logoSize = logo.getWidth();
|
||||
|
||||
Bitmap logoBitmap = Bitmap.createBitmap(qrSize, qrSize, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(logoBitmap);
|
||||
canvas.drawBitmap(qrBitmap, 0, 0, null);
|
||||
canvas.save();
|
||||
final float sx = 0.2f * qrSize / logoSize;
|
||||
canvas.scale(sx, sx, qrSize / 2f, qrSize / 2f);
|
||||
canvas.drawBitmap(logo, (qrSize - logoSize) / 2f, (qrSize - logoSize) / 2f, null);
|
||||
canvas.restore();
|
||||
return logoBitmap;
|
||||
}
|
||||
|
||||
private Bitmap logo = null;
|
||||
|
||||
private Bitmap getMoneroLogo() {
|
||||
if (logo == null) {
|
||||
logo = Helper.getBitmap(getContext(), R.drawable.ic_monero_logo_b);
|
||||
}
|
||||
return logo;
|
||||
}
|
||||
|
||||
public void showProgress() {
|
||||
pbProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void hideProgress() {
|
||||
pbProgress.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
Listener listenerCallback = null;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
this.listenerCallback = (Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
Timber.d("onPause()");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
Timber.d("onDetach()");
|
||||
if ((wallet != null) && (isMyWallet)) {
|
||||
wallet.close();
|
||||
wallet = null;
|
||||
isMyWallet = false;
|
||||
}
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
private Subaddress subaddress = null;
|
||||
|
||||
void setNewSubaddress() {
|
||||
final Subaddress newSubaddress = listenerCallback.getSelectedSubaddress();
|
||||
if (!Objects.equals(subaddress, newSubaddress)) {
|
||||
final Runnable resetSize = () -> tvAddress.animate().setDuration(125).scaleX(1).scaleY(1).start();
|
||||
tvAddress.animate().alpha(1).setDuration(125)
|
||||
.scaleX(1.2f).scaleY(1.2f)
|
||||
.withEndAction(resetSize).start();
|
||||
}
|
||||
subaddress = newSubaddress;
|
||||
final Context context = getContext();
|
||||
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),
|
||||
subaddress.getDisplayLabel(), subaddress.getAddress()));
|
||||
tvAddress.setText(label);
|
||||
generateQr();
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 dm77, 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.Result;
|
||||
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ScannerFragment extends Fragment implements ZXingScannerView.ResultHandler {
|
||||
|
||||
private OnScannedListener onScannedListener;
|
||||
|
||||
public interface OnScannedListener {
|
||||
boolean onScanned(String qrCode);
|
||||
}
|
||||
|
||||
private ZXingScannerView mScannerView;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
Timber.d("onCreateView");
|
||||
mScannerView = new ZXingScannerView(getActivity());
|
||||
return mScannerView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume");
|
||||
mScannerView.setResultHandler(this);
|
||||
mScannerView.startCamera();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
if ((rawResult.getBarcodeFormat() == BarcodeFormat.QR_CODE)) {
|
||||
if (onScannedListener.onScanned(rawResult.getText())) {
|
||||
return;
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_qr_address_invalid), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_qr_invalid), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// Note from dm77:
|
||||
// * Wait 2 seconds to resume the preview.
|
||||
// * On older devices continuously stopping and resuming camera preview can result in freezing the app.
|
||||
// * I don't know why this is the case but I don't have the time to figure out.
|
||||
Handler handler = new Handler();
|
||||
handler.postDelayed(() -> mScannerView.resumeCameraPreview(ScannerFragment.this), 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
Timber.d("onPause");
|
||||
mScannerView.stopCamera();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof OnScannedListener) {
|
||||
this.onScannedListener = (OnScannedListener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.LocaleHelper;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static android.view.WindowManager.LayoutParams;
|
||||
|
||||
public abstract class SecureActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Helper.preventScreenshot()) {
|
||||
getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(newBase);
|
||||
applyOverrideConfiguration(new Configuration());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyOverrideConfiguration(Configuration newConfig) {
|
||||
super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig));
|
||||
}
|
||||
|
||||
private Configuration updateConfigurationIfSupported(Configuration config) {
|
||||
// Configuration.getLocales is added after 24 and Configuration.locale is deprecated in 24
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (!config.getLocales().isEmpty()) {
|
||||
return config;
|
||||
}
|
||||
} else {
|
||||
if (config.locale != null) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
Locale locale = LocaleHelper.getPreferredLocale(this);
|
||||
if (locale != null) {
|
||||
config.setLocale(locale);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
package com.m2049r.xmrwallet;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.m2049r.xmrwallet.dialog.AboutFragment;
|
||||
import com.m2049r.xmrwallet.dialog.CreditsFragment;
|
||||
import com.m2049r.xmrwallet.dialog.PrivacyFragment;
|
||||
import com.m2049r.xmrwallet.util.DayNightMode;
|
||||
import com.m2049r.xmrwallet.util.LocaleHelper;
|
||||
import com.m2049r.xmrwallet.util.NightmodeHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
findPreference(getString(R.string.about_info)).setOnPreferenceClickListener(preference -> {
|
||||
AboutFragment.display(getParentFragmentManager());
|
||||
return true;
|
||||
});
|
||||
findPreference(getString(R.string.privacy_info)).setOnPreferenceClickListener(preference -> {
|
||||
PrivacyFragment.display(getParentFragmentManager());
|
||||
return true;
|
||||
});
|
||||
findPreference(getString(R.string.credits_info)).setOnPreferenceClickListener(preference -> {
|
||||
CreditsFragment.display(getParentFragmentManager());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(getString(R.string.preferred_locale))) {
|
||||
activity.recreate();
|
||||
} else if (key.equals(getString(R.string.preferred_nightmode))) {
|
||||
NightmodeHelper.setNightMode(DayNightMode.valueOf(sharedPreferences.getString(key, "AUTO")));
|
||||
} else if (key.equals(getString(R.string.preferred_theme))) {
|
||||
ThemeHelper.setTheme((Activity) activity, sharedPreferences.getString(key, "Classic"));
|
||||
activity.recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private SettingsFragment.Listener activity;
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof SettingsFragment.Listener) {
|
||||
activity = (SettingsFragment.Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context + " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activity.setSubtitle(getString(R.string.menu_settings));
|
||||
activity.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
populateLanguages();
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setSubtitle(String title);
|
||||
|
||||
void recreate();
|
||||
|
||||
void setTheme(@StyleRes final int resId);
|
||||
}
|
||||
|
||||
public void populateLanguages() {
|
||||
ListPreference language = findPreference(getString(R.string.preferred_locale));
|
||||
assert language != null;
|
||||
|
||||
final ArrayList<Locale> availableLocales = LocaleHelper.getAvailableLocales(requireContext());
|
||||
Collections.sort(availableLocales, (locale1, locale2) -> {
|
||||
String localeString1 = LocaleHelper.getDisplayName(locale1, true);
|
||||
String localeString2 = LocaleHelper.getDisplayName(locale2, true);
|
||||
return localeString1.compareTo(localeString2);
|
||||
});
|
||||
|
||||
String[] localeDisplayNames = new String[1 + availableLocales.size()];
|
||||
localeDisplayNames[0] = getString(R.string.language_system_default);
|
||||
for (int i = 1; i < localeDisplayNames.length; i++) {
|
||||
localeDisplayNames[i] = LocaleHelper.getDisplayName(availableLocales.get(i - 1), true);
|
||||
}
|
||||
language.setEntries(localeDisplayNames);
|
||||
|
||||
String[] languageTags = new String[1 + availableLocales.size()];
|
||||
languageTags[0] = "";
|
||||
for (int i = 1; i < languageTags.length; i++) {
|
||||
languageTags[i] = availableLocales.get(i - 1).toLanguageTag();
|
||||
}
|
||||
language.setEntryValues(languageTags);
|
||||
}
|
||||
}
|
@ -1,241 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.m2049r.xmrwallet.data.Subaddress;
|
||||
import com.m2049r.xmrwallet.layout.SubaddressInfoAdapter;
|
||||
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SubaddressFragment extends Fragment implements SubaddressInfoAdapter.OnInteractionListener,
|
||||
View.OnClickListener, OnBlockUpdateListener {
|
||||
static public final String KEY_MODE = "mode";
|
||||
static public final String MODE_MANAGER = "manager";
|
||||
|
||||
private SubaddressInfoAdapter adapter;
|
||||
|
||||
private Listener activityCallback;
|
||||
|
||||
private Wallet wallet;
|
||||
|
||||
// Container Activity must implement this interface
|
||||
public interface Listener {
|
||||
void onSubaddressSelected(Subaddress subaddress);
|
||||
|
||||
void setSubtitle(String title);
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void showSubaddress(View view, final int subaddressIndex);
|
||||
|
||||
void saveWallet();
|
||||
}
|
||||
|
||||
public interface ProgressListener {
|
||||
void showProgressDialog(int msgId);
|
||||
|
||||
void showLedgerProgressDialog(int mode);
|
||||
|
||||
void dismissProgressDialog();
|
||||
}
|
||||
|
||||
private ProgressListener progressCallback = null;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof ProgressListener) {
|
||||
progressCallback = (ProgressListener) context;
|
||||
}
|
||||
if (context instanceof Listener) {
|
||||
activityCallback = (Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
Timber.d("onPause()");
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
activityCallback.setSubtitle(getString(R.string.subbaddress_title));
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
Timber.d("onCreateView");
|
||||
|
||||
final Bundle b = getArguments();
|
||||
managerMode = ((b != null) && (MODE_MANAGER.equals(b.getString(KEY_MODE))));
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_subaddress, container, false);
|
||||
view.findViewById(R.id.fab).setOnClickListener(this);
|
||||
|
||||
if (managerMode) {
|
||||
view.findViewById(R.id.tvInstruction).setVisibility(View.GONE);
|
||||
view.findViewById(R.id.tvHint).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final RecyclerView list = view.findViewById(R.id.list);
|
||||
adapter = new SubaddressInfoAdapter(getActivity(), this);
|
||||
list.setAdapter(adapter);
|
||||
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
list.scrollToPosition(positionStart);
|
||||
}
|
||||
});
|
||||
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
wallet = WalletManager.getInstance().getWallet();
|
||||
|
||||
loadList();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
public void loadList() {
|
||||
Timber.d("loadList()");
|
||||
final int numSubaddresses = wallet.getNumSubaddresses();
|
||||
final List<Subaddress> list = new ArrayList<>();
|
||||
for (int i = 0; i < numSubaddresses; i++) {
|
||||
list.add(wallet.getSubaddressObject(i));
|
||||
}
|
||||
adapter.setInfos(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockUpdate(Wallet wallet) {
|
||||
loadList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int id = v.getId();
|
||||
if (id == R.id.fab) {
|
||||
getNewSubaddress();
|
||||
}
|
||||
}
|
||||
|
||||
private int lastUsedSubaddress() {
|
||||
int lastUsedSubaddress = 0;
|
||||
for (TransactionInfo info : wallet.getHistory().getAll()) {
|
||||
if (info.addressIndex > lastUsedSubaddress)
|
||||
lastUsedSubaddress = info.addressIndex;
|
||||
}
|
||||
return lastUsedSubaddress;
|
||||
}
|
||||
|
||||
private void getNewSubaddress() {
|
||||
final int maxSubaddresses = lastUsedSubaddress() + wallet.getDeviceType().getSubaddressLookahead();
|
||||
if (wallet.getNumSubaddresses() < maxSubaddresses)
|
||||
new AsyncSubaddress().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR);
|
||||
else
|
||||
Toast.makeText(getActivity(), getString(R.string.max_subaddress_warning), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@RequiredArgsConstructor
|
||||
private class AsyncSubaddress extends AsyncTask<Void, Void, Boolean> {
|
||||
boolean dialogOpened = false;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
if ((wallet.getDeviceType() == Wallet.Device.Device_Ledger) && (progressCallback != null)) {
|
||||
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS);
|
||||
dialogOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
if (params.length != 0) return false;
|
||||
wallet.getNewSubaddress();
|
||||
if (activityCallback != null) {
|
||||
activityCallback.saveWallet();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
if (dialogOpened)
|
||||
progressCallback.dismissProgressDialog();
|
||||
if (!isAdded()) // never mind then
|
||||
return;
|
||||
loadList();
|
||||
}
|
||||
}
|
||||
|
||||
boolean managerMode = false;
|
||||
|
||||
// Callbacks from SubaddressInfoAdapter
|
||||
@Override
|
||||
public void onInteraction(final View view, final Subaddress subaddress) {
|
||||
if (managerMode)
|
||||
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
|
||||
else
|
||||
activityCallback.onSubaddressSelected(subaddress); // also closes the fragment with onBackpressed()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongInteraction(View view, Subaddress subaddress) {
|
||||
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.Transition;
|
||||
import androidx.transition.TransitionInflater;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.data.Subaddress;
|
||||
import com.m2049r.xmrwallet.layout.TransactionInfoAdapter;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SubaddressInfoFragment extends Fragment
|
||||
implements TransactionInfoAdapter.OnInteractionListener, OnBlockUpdateListener {
|
||||
private TransactionInfoAdapter adapter;
|
||||
|
||||
private Subaddress subaddress;
|
||||
|
||||
private TextInputLayout etName;
|
||||
private TextView tvAddress;
|
||||
private TextView tvTxLabel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
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);
|
||||
adapter = new TransactionInfoAdapter(getActivity(), this);
|
||||
list.setAdapter(adapter);
|
||||
|
||||
final Wallet wallet = activityCallback.getWallet();
|
||||
|
||||
Bundle b = getArguments();
|
||||
final int subaddressIndex = b.getInt("subaddressIndex");
|
||||
subaddress = wallet.getSubaddressObject(subaddressIndex);
|
||||
|
||||
etName.getEditText().setText(subaddress.getDisplayLabel());
|
||||
tvAddress.setText(getContext().getString(R.string.subbaddress_info_subtitle,
|
||||
subaddress.getAddressIndex(), subaddress.getSquashedAddress()));
|
||||
|
||||
etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString());
|
||||
}
|
||||
});
|
||||
etName.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
Helper.hideKeyboard(getActivity());
|
||||
wallet.setSubaddressLabel(subaddressIndex, etName.getEditText().getText().toString());
|
||||
onRefreshed(wallet);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
onRefreshed(wallet);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Transition transform = TransitionInflater.from(requireContext())
|
||||
.inflateTransition(R.transition.details);
|
||||
setSharedElementEnterTransition(transform);
|
||||
}
|
||||
|
||||
public void onRefreshed(final Wallet wallet) {
|
||||
Timber.d("onRefreshed");
|
||||
List<TransactionInfo> list = new ArrayList<>();
|
||||
for (TransactionInfo info : wallet.getHistory().getAll()) {
|
||||
if (info.addressIndex == subaddress.getAddressIndex())
|
||||
list.add(info);
|
||||
}
|
||||
adapter.setInfos(list);
|
||||
if (list.isEmpty())
|
||||
tvTxLabel.setText(R.string.subaddress_notx_label);
|
||||
else
|
||||
tvTxLabel.setText(R.string.subaddress_tx_label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockUpdate(Wallet wallet) {
|
||||
onRefreshed(wallet);
|
||||
}
|
||||
|
||||
// Callbacks from TransactionInfoAdapter
|
||||
@Override
|
||||
public void onInteraction(final View view, final TransactionInfo infoItem) {
|
||||
activityCallback.onTxDetailsRequest(view, infoItem);
|
||||
}
|
||||
|
||||
Listener activityCallback;
|
||||
|
||||
// Container Activity must implement this interface
|
||||
public interface Listener {
|
||||
void onTxDetailsRequest(View view, TransactionInfo info);
|
||||
|
||||
Wallet getWallet();
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setTitle(String title, String subtitle);
|
||||
|
||||
void setSubtitle(String subtitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
this.activityCallback = (Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activityCallback.setSubtitle(getString(R.string.subbaddress_title));
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
}
|
@ -1,406 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
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;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.transition.Transition;
|
||||
import androidx.transition.TransitionInflater;
|
||||
|
||||
import com.m2049r.xmrwallet.data.Subaddress;
|
||||
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.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class TxFragment extends Fragment {
|
||||
|
||||
static public final String ARG_INFO = "info";
|
||||
|
||||
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
|
||||
|
||||
public TxFragment() {
|
||||
super();
|
||||
Calendar cal = Calendar.getInstance();
|
||||
TimeZone tz = cal.getTimeZone(); //get the local time zone.
|
||||
TS_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
private TextView tvAccount;
|
||||
private TextView tvAddress;
|
||||
private TextView tvTxTimestamp;
|
||||
private TextView tvTxId;
|
||||
private TextView tvTxKey;
|
||||
private TextView tvDestination;
|
||||
private TextView tvTxPaymentId;
|
||||
private TextView tvTxBlockheight;
|
||||
private TextView tvTxAmount;
|
||||
private TextView tvTxFee;
|
||||
private TextView tvTxTransfers;
|
||||
private TextView etTxNotes;
|
||||
|
||||
// XMRTO stuff
|
||||
private View cvXmrTo;
|
||||
private TextView tvTxXmrToKey;
|
||||
private TextView tvDestinationBtc;
|
||||
private TextView tvTxAmountBtc;
|
||||
private TextView tvXmrToSupport;
|
||||
private TextView tvXmrToKeyLabel;
|
||||
private ImageView tvXmrToLogo;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_tx_info, container, false);
|
||||
|
||||
cvXmrTo = view.findViewById(R.id.cvXmrTo);
|
||||
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
||||
tvDestinationBtc = view.findViewById(R.id.tvDestinationBtc);
|
||||
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);
|
||||
tvAddress = view.findViewById(R.id.tvAddress);
|
||||
tvTxTimestamp = view.findViewById(R.id.tvTxTimestamp);
|
||||
tvTxId = view.findViewById(R.id.tvTxId);
|
||||
tvTxKey = view.findViewById(R.id.tvTxKey);
|
||||
tvDestination = view.findViewById(R.id.tvDestination);
|
||||
tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId);
|
||||
tvTxBlockheight = view.findViewById(R.id.tvTxBlockheight);
|
||||
tvTxAmount = view.findViewById(R.id.tvTxAmount);
|
||||
tvTxFee = view.findViewById(R.id.tvTxFee);
|
||||
tvTxTransfers = view.findViewById(R.id.tvTxTransfers);
|
||||
etTxNotes = view.findViewById(R.id.etTxNotes);
|
||||
|
||||
etTxNotes.setRawInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
tvTxXmrToKey.setOnClickListener(v -> {
|
||||
Helper.clipBoardCopy(getActivity(), 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);
|
||||
show();
|
||||
return view;
|
||||
}
|
||||
|
||||
void shareTxInfo() {
|
||||
if (this.info == null) return;
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
sb.append(getString(R.string.tx_timestamp)).append(":\n");
|
||||
sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n\n");
|
||||
|
||||
sb.append(getString(R.string.tx_amount)).append(":\n");
|
||||
sb.append((info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-"));
|
||||
sb.append(Wallet.getDisplayAmount(info.amount)).append("\n");
|
||||
sb.append(getString(R.string.tx_fee)).append(":\n");
|
||||
sb.append(Wallet.getDisplayAmount(info.fee)).append("\n\n");
|
||||
|
||||
sb.append(getString(R.string.tx_notes)).append(":\n");
|
||||
String oneLineNotes = info.notes.replace("\n", " ; ");
|
||||
sb.append(oneLineNotes.isEmpty() ? "-" : oneLineNotes).append("\n\n");
|
||||
|
||||
sb.append(getString(R.string.tx_destination)).append(":\n");
|
||||
sb.append(tvDestination.getText()).append("\n\n");
|
||||
|
||||
sb.append(getString(R.string.tx_paymentId)).append(":\n");
|
||||
sb.append(info.paymentId).append("\n\n");
|
||||
|
||||
sb.append(getString(R.string.tx_id)).append(":\n");
|
||||
sb.append(info.hash).append("\n");
|
||||
sb.append(getString(R.string.tx_key)).append(":\n");
|
||||
sb.append(info.txKey.isEmpty() ? "-" : info.txKey).append("\n\n");
|
||||
|
||||
sb.append(getString(R.string.tx_blockheight)).append(":\n");
|
||||
if (info.isFailed) {
|
||||
sb.append(getString(R.string.tx_failed)).append("\n");
|
||||
} else if (info.isPending) {
|
||||
sb.append(getString(R.string.tx_pending)).append("\n");
|
||||
} else {
|
||||
sb.append(info.blockheight).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
sb.append(getString(R.string.tx_transfers)).append(":\n");
|
||||
if (info.transfers != null) {
|
||||
boolean comma = false;
|
||||
for (Transfer transfer : info.transfers) {
|
||||
if (comma) {
|
||||
sb.append(", ");
|
||||
} else {
|
||||
comma = true;
|
||||
}
|
||||
sb.append(transfer.address).append(": ");
|
||||
sb.append(Wallet.getDisplayAmount(transfer.amount));
|
||||
}
|
||||
} else {
|
||||
sb.append("-");
|
||||
}
|
||||
sb.append("\n\n");
|
||||
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
|
||||
sendIntent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(sendIntent, null));
|
||||
}
|
||||
|
||||
TransactionInfo info = null;
|
||||
UserNotes userNotes = null;
|
||||
|
||||
void loadNotes() {
|
||||
if ((userNotes == null) || (info.notes == null)) {
|
||||
info.notes = activityCallback.getTxNotes(info.hash);
|
||||
}
|
||||
userNotes = new UserNotes(info.notes);
|
||||
etTxNotes.setText(userNotes.note);
|
||||
}
|
||||
|
||||
private void setTxColour(int clr) {
|
||||
tvTxAmount.setTextColor(clr);
|
||||
tvTxFee.setTextColor(clr);
|
||||
}
|
||||
|
||||
private void showSubaddressLabel() {
|
||||
final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex);
|
||||
final Context ctx = getContext();
|
||||
Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted,
|
||||
info.accountIndex, info.addressIndex,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
subaddress.getDisplayLabel()));
|
||||
tvAccount.setText(label);
|
||||
tvAccount.setOnClickListener(v -> activityCallback.showSubaddress(v, info.addressIndex));
|
||||
}
|
||||
|
||||
private void show() {
|
||||
if (info.txKey == null) {
|
||||
info.txKey = activityCallback.getTxKey(info.hash);
|
||||
}
|
||||
if (info.address == null) {
|
||||
info.address = activityCallback.getTxAddress(info.accountIndex, info.addressIndex);
|
||||
}
|
||||
loadNotes();
|
||||
|
||||
showSubaddressLabel();
|
||||
tvAddress.setText(info.address);
|
||||
|
||||
tvTxTimestamp.setText(TS_FORMATTER.format(new Date(info.timestamp * 1000)));
|
||||
tvTxId.setText(info.hash);
|
||||
tvTxKey.setText(info.txKey.isEmpty() ? "-" : info.txKey);
|
||||
tvTxPaymentId.setText(info.paymentId);
|
||||
if (info.isFailed) {
|
||||
tvTxBlockheight.setText(getString(R.string.tx_failed));
|
||||
} else if (info.isPending) {
|
||||
tvTxBlockheight.setText(getString(R.string.tx_pending));
|
||||
} else {
|
||||
tvTxBlockheight.setText("" + info.blockheight);
|
||||
}
|
||||
String sign = (info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-");
|
||||
|
||||
long realAmount = info.amount;
|
||||
tvTxAmount.setText(sign + Wallet.getDisplayAmount(realAmount));
|
||||
|
||||
if ((info.fee > 0)) {
|
||||
String fee = Wallet.getDisplayAmount(info.fee);
|
||||
tvTxFee.setText(getString(R.string.tx_list_fee, fee));
|
||||
} else {
|
||||
tvTxFee.setText(null);
|
||||
tvTxFee.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
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));
|
||||
} else if (info.isPending) {
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
|
||||
} else if (info.direction == TransactionInfo.Direction.Direction_In) {
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor));
|
||||
} else {
|
||||
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor));
|
||||
}
|
||||
Set<String> destinations = new HashSet<>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
StringBuilder dstSb = new StringBuilder();
|
||||
if (info.transfers != null) {
|
||||
boolean newline = false;
|
||||
for (Transfer transfer : info.transfers) {
|
||||
destinations.add(transfer.address);
|
||||
if (newline) {
|
||||
sb.append("\n");
|
||||
} else {
|
||||
newline = true;
|
||||
}
|
||||
sb.append("[").append(transfer.address.substring(0, 6)).append("] ");
|
||||
sb.append(Wallet.getDisplayAmount(transfer.amount));
|
||||
}
|
||||
newline = false;
|
||||
for (String dst : destinations) {
|
||||
if (newline) {
|
||||
dstSb.append("\n");
|
||||
} else {
|
||||
newline = true;
|
||||
}
|
||||
dstSb.append(dst);
|
||||
}
|
||||
} else {
|
||||
sb.append("-");
|
||||
dstSb.append(info.direction == TransactionInfo.Direction.Direction_In ?
|
||||
activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex).getAddress() :
|
||||
"-");
|
||||
}
|
||||
tvTxTransfers.setText(sb.toString());
|
||||
tvDestination.setText(dstSb.toString());
|
||||
showBtcInfo();
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
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);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
cvXmrTo.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
Transition transform = TransitionInflater.from(requireContext())
|
||||
.inflateTransition(R.transition.details);
|
||||
setSharedElementEnterTransition(transform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.tx_info_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
Listener activityCallback;
|
||||
|
||||
public interface Listener {
|
||||
Subaddress getWalletSubaddress(int accountIndex, int subaddressIndex);
|
||||
|
||||
String getTxKey(String hash);
|
||||
|
||||
String getTxNotes(String hash);
|
||||
|
||||
boolean setTxNotes(String txId, String txNotes);
|
||||
|
||||
String getTxAddress(int major, int minor);
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setSubtitle(String subtitle);
|
||||
|
||||
void showSubaddress(View view, final int subaddressIndex);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof TxFragment.Listener) {
|
||||
this.activityCallback = (TxFragment.Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (!etTxNotes.getText().toString().equals(userNotes.note)) { // notes have changed
|
||||
// save them
|
||||
userNotes.setNote(etTxNotes.getText().toString());
|
||||
info.notes = userNotes.txNotes;
|
||||
activityCallback.setTxNotes(info.hash, info.notes);
|
||||
}
|
||||
Helper.hideKeyboard(getActivity());
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activityCallback.setSubtitle(getString(R.string.tx_title));
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
|
||||
showSubaddressLabel();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,559 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.github.brnunes.swipeablerecyclerview.SwipeableRecyclerViewTouchListener;
|
||||
import com.m2049r.xmrwallet.layout.TransactionInfoAdapter;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class WalletFragment extends Fragment
|
||||
implements TransactionInfoAdapter.OnInteractionListener {
|
||||
private TransactionInfoAdapter adapter;
|
||||
private final NumberFormat formatter = NumberFormat.getInstance();
|
||||
|
||||
private TextView tvStreetView;
|
||||
private LinearLayout llBalance;
|
||||
private FrameLayout flExchange;
|
||||
private TextView tvBalance;
|
||||
private TextView tvUnconfirmedAmount;
|
||||
private TextView tvProgress;
|
||||
private ImageView ivSynced;
|
||||
private ProgressBar pbProgress;
|
||||
private Button bReceive;
|
||||
private Button bSend;
|
||||
private ImageView ivStreetGunther;
|
||||
private Drawable streetGunther = null;
|
||||
RecyclerView txlist;
|
||||
|
||||
private Spinner sCurrency;
|
||||
|
||||
private final List<String> dismissedTransactions = new ArrayList<>();
|
||||
|
||||
public void resetDismissedTransactions() {
|
||||
dismissedTransactions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
if (activityCallback.hasWallet())
|
||||
inflater.inflate(R.menu.wallet_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_wallet, container, false);
|
||||
|
||||
ivStreetGunther = view.findViewById(R.id.ivStreetGunther);
|
||||
tvStreetView = view.findViewById(R.id.tvStreetView);
|
||||
llBalance = view.findViewById(R.id.llBalance);
|
||||
flExchange = view.findViewById(R.id.flExchange);
|
||||
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
|
||||
setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tvProgress = view.findViewById(R.id.tvProgress);
|
||||
pbProgress = view.findViewById(R.id.pbProgress);
|
||||
tvBalance = view.findViewById(R.id.tvBalance);
|
||||
showBalance(Helper.getDisplayAmount(0));
|
||||
tvUnconfirmedAmount = view.findViewById(R.id.tvUnconfirmedAmount);
|
||||
showUnconfirmed(0);
|
||||
ivSynced = view.findViewById(R.id.ivSynced);
|
||||
|
||||
sCurrency = view.findViewById(R.id.sCurrency);
|
||||
List<String> currencies = new ArrayList<>();
|
||||
currencies.add(Helper.BASE_CRYPTO);
|
||||
if (Helper.SHOW_EXCHANGERATES)
|
||||
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(requireContext(), R.layout.item_spinner_balance, currencies);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
sCurrency.setAdapter(spinnerAdapter);
|
||||
|
||||
bSend = view.findViewById(R.id.bSend);
|
||||
bReceive = view.findViewById(R.id.bReceive);
|
||||
|
||||
txlist = view.findViewById(R.id.list);
|
||||
adapter = new TransactionInfoAdapter(getActivity(), this);
|
||||
txlist.setAdapter(adapter);
|
||||
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
if ((positionStart == 0) && (txlist.computeVerticalScrollOffset() == 0))
|
||||
txlist.scrollToPosition(positionStart);
|
||||
}
|
||||
});
|
||||
|
||||
txlist.addOnItemTouchListener(
|
||||
new SwipeableRecyclerViewTouchListener(txlist,
|
||||
new SwipeableRecyclerViewTouchListener.SwipeListener() {
|
||||
@Override
|
||||
public boolean canSwipeLeft(int position) {
|
||||
return activityCallback.isStreetMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canSwipeRight(int position) {
|
||||
return activityCallback.isStreetMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissedBySwipeLeft(RecyclerView recyclerView, int[] reverseSortedPositions) {
|
||||
for (int position : reverseSortedPositions) {
|
||||
dismissedTransactions.add(adapter.getItem(position).hash);
|
||||
adapter.removeItem(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissedBySwipeRight(RecyclerView recyclerView, int[] reverseSortedPositions) {
|
||||
for (int position : reverseSortedPositions) {
|
||||
dismissedTransactions.add(adapter.getItem(position).hash);
|
||||
adapter.removeItem(position);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
bSend.setOnClickListener(v -> activityCallback.onSendRequest(v));
|
||||
bReceive.setOnClickListener(v -> activityCallback.onWalletReceive(v));
|
||||
|
||||
sCurrency.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
|
||||
refreshBalance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parentView) {
|
||||
// nothing (yet?)
|
||||
}
|
||||
});
|
||||
|
||||
if (activityCallback.isSynced()) {
|
||||
onSynced();
|
||||
}
|
||||
|
||||
activityCallback.forceUpdate();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
void showBalance(String balance) {
|
||||
tvBalance.setText(balance);
|
||||
final boolean streetMode = activityCallback.isStreetMode();
|
||||
if (!streetMode) {
|
||||
llBalance.setVisibility(View.VISIBLE);
|
||||
tvStreetView.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
llBalance.setVisibility(View.INVISIBLE);
|
||||
tvStreetView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
setStreetModeBackground(streetMode);
|
||||
}
|
||||
|
||||
void showUnconfirmed(double unconfirmedAmount) {
|
||||
if (activityCallback.isStreetMode() || unconfirmedAmount == 0) {
|
||||
tvUnconfirmedAmount.setText(null);
|
||||
tvUnconfirmedAmount.setVisibility(View.GONE);
|
||||
} else {
|
||||
String unconfirmed = Helper.getFormattedAmount(unconfirmedAmount, true);
|
||||
tvUnconfirmedAmount.setText(getResources().getString(R.string.xmr_unconfirmed_amount, unconfirmed));
|
||||
tvUnconfirmedAmount.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
void updateBalance() {
|
||||
if (isExchanging) return; // wait for exchange to finish - it will fire this itself then.
|
||||
// at this point selection is XMR in case of error
|
||||
String displayB;
|
||||
double amountA = Helper.getDecimalAmount(unlockedBalance).doubleValue();
|
||||
if (!Helper.BASE_CRYPTO.equals(balanceCurrency)) { // not XMR
|
||||
double amountB = amountA * balanceRate;
|
||||
displayB = Helper.getFormattedAmount(amountB, false);
|
||||
} else { // XMR
|
||||
displayB = Helper.getFormattedAmount(amountA, true);
|
||||
}
|
||||
showBalance(displayB);
|
||||
}
|
||||
|
||||
String balanceCurrency = Helper.BASE_CRYPTO;
|
||||
double balanceRate = 1.0;
|
||||
|
||||
private final ExchangeApi exchangeApi = ServiceHelper.getExchangeApi();
|
||||
|
||||
void refreshBalance() {
|
||||
double unconfirmedXmr = Helper.getDecimalAmount(balance - unlockedBalance).doubleValue();
|
||||
showUnconfirmed(unconfirmedXmr);
|
||||
if (sCurrency.getSelectedItemPosition() == 0) { // XMR
|
||||
double amountXmr = Helper.getDecimalAmount(unlockedBalance).doubleValue();
|
||||
showBalance(Helper.getFormattedAmount(amountXmr, true));
|
||||
} else { // not XMR
|
||||
String currency = (String) sCurrency.getSelectedItem();
|
||||
Timber.d(currency);
|
||||
if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) {
|
||||
showExchanging();
|
||||
exchangeApi.queryExchangeRate(Helper.BASE_CRYPTO, currency,
|
||||
new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
if (isAdded())
|
||||
new Handler(Looper.getMainLooper()).post(() -> exchange(exchangeRate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
Timber.e(e.getLocalizedMessage());
|
||||
if (isAdded())
|
||||
new Handler(Looper.getMainLooper()).post(() -> exchangeFailed());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updateBalance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isExchanging = false;
|
||||
|
||||
void showExchanging() {
|
||||
isExchanging = true;
|
||||
tvBalance.setVisibility(View.GONE);
|
||||
flExchange.setVisibility(View.VISIBLE);
|
||||
sCurrency.setEnabled(false);
|
||||
}
|
||||
|
||||
void hideExchanging() {
|
||||
isExchanging = false;
|
||||
tvBalance.setVisibility(View.VISIBLE);
|
||||
flExchange.setVisibility(View.GONE);
|
||||
sCurrency.setEnabled(true);
|
||||
}
|
||||
|
||||
public void exchangeFailed() {
|
||||
sCurrency.setSelection(0, true); // default to XMR
|
||||
double amountXmr = Helper.getDecimalAmount(unlockedBalance).doubleValue();
|
||||
showBalance(Helper.getFormattedAmount(amountXmr, true));
|
||||
hideExchanging();
|
||||
}
|
||||
|
||||
public void exchange(final ExchangeRate exchangeRate) {
|
||||
hideExchanging();
|
||||
if (!Helper.BASE_CRYPTO.equals(exchangeRate.getBaseCurrency())) {
|
||||
Timber.e("Not XMR");
|
||||
sCurrency.setSelection(0, true);
|
||||
balanceCurrency = Helper.BASE_CRYPTO;
|
||||
balanceRate = 1.0;
|
||||
} else {
|
||||
int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(exchangeRate.getQuoteCurrency());
|
||||
if (spinnerPosition < 0) { // requested currency not in list
|
||||
Timber.e("Requested currency not in list %s", exchangeRate.getQuoteCurrency());
|
||||
sCurrency.setSelection(0, true);
|
||||
} else {
|
||||
sCurrency.setSelection(spinnerPosition, true);
|
||||
}
|
||||
balanceCurrency = exchangeRate.getQuoteCurrency();
|
||||
balanceRate = exchangeRate.getRate();
|
||||
}
|
||||
updateBalance();
|
||||
}
|
||||
|
||||
// Callbacks from TransactionInfoAdapter
|
||||
@Override
|
||||
public void onInteraction(final View view, final TransactionInfo infoItem) {
|
||||
activityCallback.onTxDetailsRequest(view, infoItem);
|
||||
}
|
||||
|
||||
// if account index has changed scroll to top?
|
||||
private int accountIndex = 0;
|
||||
|
||||
public void onRefreshed(final Wallet wallet, boolean full) {
|
||||
Timber.d("onRefreshed(%b)", full);
|
||||
|
||||
if (adapter.needsTransactionUpdateOnNewBlock()) {
|
||||
wallet.refreshHistory();
|
||||
full = true;
|
||||
}
|
||||
if (full) {
|
||||
List<TransactionInfo> list = new ArrayList<>();
|
||||
final long streetHeight = activityCallback.getStreetModeHeight();
|
||||
Timber.d("StreetHeight=%d", streetHeight);
|
||||
wallet.refreshHistory();
|
||||
for (TransactionInfo info : wallet.getHistory().getAll()) {
|
||||
Timber.d("TxHeight=%d, Label=%s", info.blockheight, info.subaddressLabel);
|
||||
if ((info.isPending || (info.blockheight >= streetHeight))
|
||||
&& !dismissedTransactions.contains(info.hash))
|
||||
list.add(info);
|
||||
}
|
||||
adapter.setInfos(list);
|
||||
if (accountIndex != wallet.getAccountIndex()) {
|
||||
accountIndex = wallet.getAccountIndex();
|
||||
txlist.scrollToPosition(0);
|
||||
}
|
||||
}
|
||||
updateStatus(wallet);
|
||||
}
|
||||
|
||||
public void onSynced() {
|
||||
if (!activityCallback.isWatchOnly()) {
|
||||
bSend.setVisibility(View.VISIBLE);
|
||||
bSend.setEnabled(true);
|
||||
}
|
||||
if (isVisible()) enableAccountsList(true); //otherwise it is enabled in onResume()
|
||||
}
|
||||
|
||||
public void unsync() {
|
||||
if (!activityCallback.isWatchOnly()) {
|
||||
bSend.setVisibility(View.INVISIBLE);
|
||||
bSend.setEnabled(false);
|
||||
}
|
||||
if (isVisible()) enableAccountsList(false); //otherwise it is enabled in onResume()
|
||||
firstBlock = 0;
|
||||
}
|
||||
|
||||
boolean walletLoaded = false;
|
||||
|
||||
public void onLoaded() {
|
||||
walletLoaded = true;
|
||||
showReceive();
|
||||
}
|
||||
|
||||
private void showReceive() {
|
||||
if (walletLoaded) {
|
||||
bReceive.setVisibility(View.VISIBLE);
|
||||
bReceive.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private String syncText = null;
|
||||
|
||||
public void setProgress(final String text) {
|
||||
syncText = text;
|
||||
tvProgress.setText(text);
|
||||
}
|
||||
|
||||
private int syncProgress = -1;
|
||||
|
||||
public void setProgress(final int n) {
|
||||
syncProgress = n;
|
||||
if (n > 100) {
|
||||
pbProgress.setIndeterminate(true);
|
||||
pbProgress.setVisibility(View.VISIBLE);
|
||||
} else if (n >= 0) {
|
||||
pbProgress.setIndeterminate(false);
|
||||
pbProgress.setProgress(n);
|
||||
pbProgress.setVisibility(View.VISIBLE);
|
||||
} else { // <0
|
||||
pbProgress.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
void setActivityTitle(Wallet wallet) {
|
||||
if (wallet == null) return;
|
||||
walletTitle = wallet.getName();
|
||||
walletSubtitle = wallet.getAccountLabel();
|
||||
activityCallback.setTitle(walletTitle, walletSubtitle);
|
||||
Timber.d("wallet title is %s", walletTitle);
|
||||
}
|
||||
|
||||
private long firstBlock = 0;
|
||||
private String walletTitle = null;
|
||||
private String walletSubtitle = null;
|
||||
private long unlockedBalance = 0;
|
||||
private long balance = 0;
|
||||
|
||||
private int accountIdx = -1;
|
||||
|
||||
private void updateStatus(Wallet wallet) {
|
||||
if (!isAdded()) return;
|
||||
Timber.d("updateStatus()");
|
||||
if ((walletTitle == null) || (accountIdx != wallet.getAccountIndex())) {
|
||||
accountIdx = wallet.getAccountIndex();
|
||||
setActivityTitle(wallet);
|
||||
}
|
||||
balance = wallet.getBalance();
|
||||
unlockedBalance = wallet.getUnlockedBalance();
|
||||
refreshBalance();
|
||||
String sync;
|
||||
if (!activityCallback.hasBoundService())
|
||||
throw new IllegalStateException("WalletService not bound.");
|
||||
Wallet.ConnectionStatus daemonConnected = activityCallback.getConnectionStatus();
|
||||
if (daemonConnected == Wallet.ConnectionStatus.ConnectionStatus_Connected) {
|
||||
if (!wallet.isSynchronized()) {
|
||||
long daemonHeight = activityCallback.getDaemonHeight();
|
||||
long walletHeight = wallet.getBlockChainHeight();
|
||||
long n = daemonHeight - walletHeight;
|
||||
sync = getString(R.string.status_syncing) + " " + formatter.format(n) + " " + getString(R.string.status_remaining);
|
||||
if (firstBlock == 0) {
|
||||
firstBlock = walletHeight;
|
||||
}
|
||||
int x = 100 - Math.round(100f * n / (1f * daemonHeight - firstBlock));
|
||||
if (x == 0) x = 101; // indeterminate
|
||||
setProgress(x);
|
||||
ivSynced.setVisibility(View.GONE);
|
||||
} else {
|
||||
sync = getString(R.string.status_synced) + " " + formatter.format(wallet.getBlockChainHeight());
|
||||
ivSynced.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
sync = getString(R.string.status_wallet_connecting);
|
||||
setProgress(101);
|
||||
}
|
||||
setProgress(sync);
|
||||
// TODO show connected status somewhere
|
||||
}
|
||||
|
||||
Listener activityCallback;
|
||||
|
||||
// Container Activity must implement this interface
|
||||
public interface Listener {
|
||||
boolean hasBoundService();
|
||||
|
||||
void forceUpdate();
|
||||
|
||||
Wallet.ConnectionStatus getConnectionStatus();
|
||||
|
||||
long getDaemonHeight(); //mBoundService.getDaemonHeight();
|
||||
|
||||
void onSendRequest(View view);
|
||||
|
||||
void onTxDetailsRequest(View view, TransactionInfo info);
|
||||
|
||||
boolean isSynced();
|
||||
|
||||
boolean isStreetMode();
|
||||
|
||||
long getStreetModeHeight();
|
||||
|
||||
boolean isWatchOnly();
|
||||
|
||||
String getTxKey(String txId);
|
||||
|
||||
void onWalletReceive(View view);
|
||||
|
||||
boolean hasWallet();
|
||||
|
||||
Wallet getWallet();
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setTitle(String title, String subtitle);
|
||||
|
||||
void setSubtitle(String subtitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
this.activityCallback = (Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume()");
|
||||
activityCallback.setTitle(walletTitle, walletSubtitle);
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
|
||||
setProgress(syncProgress);
|
||||
setProgress(syncText);
|
||||
showReceive();
|
||||
if (activityCallback.isSynced()) enableAccountsList(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
enableAccountsList(false);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public interface DrawerLocker {
|
||||
void setDrawerEnabled(boolean enabled);
|
||||
}
|
||||
|
||||
private void enableAccountsList(boolean enable) {
|
||||
if (activityCallback instanceof DrawerLocker) {
|
||||
((DrawerLocker) activityCallback).setDrawerEnabled(enable);
|
||||
}
|
||||
}
|
||||
|
||||
public void setStreetModeBackground(boolean enable) {
|
||||
//TODO figure out why gunther disappears on return from send although he is still set
|
||||
if (enable) {
|
||||
if (streetGunther == null)
|
||||
streetGunther = ContextCompat.getDrawable(requireContext(), R.drawable.ic_gunther_streetmode);
|
||||
ivStreetGunther.setImageDrawable(streetGunther);
|
||||
} else
|
||||
ivStreetGunther.setImageDrawable(null);
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.util.LocaleHelper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
import com.m2049r.xmrwallet.util.NightmodeHelper;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class XmrWalletApplication extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
FragmentManager.enableNewStateManager(false);
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
}
|
||||
|
||||
NightmodeHelper.setPreferredNightmode(this);
|
||||
|
||||
NetCipherHelper.createInstance(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context context) {
|
||||
super.attachBaseContext(LocaleHelper.setPreferredLocale(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration configuration) {
|
||||
super.onConfigurationChanged(configuration);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
LocaleHelper.updateSystemDefaultLocale(configuration.getLocales().get(0));
|
||||
} else {
|
||||
LocaleHelper.updateSystemDefaultLocale(configuration.locale);
|
||||
}
|
||||
LocaleHelper.setPreferredLocale(this);
|
||||
}
|
||||
|
||||
static public NetworkType getNetworkType() {
|
||||
switch (BuildConfig.FLAVOR_net) {
|
||||
case "mainnet":
|
||||
return NetworkType.NetworkType_Mainnet;
|
||||
case "stagenet":
|
||||
return NetworkType.NetworkType_Stagenet;
|
||||
case "devnet": // flavors cannot start with "test"
|
||||
return NetworkType.NetworkType_Testnet;
|
||||
default:
|
||||
throw new IllegalStateException("unknown net flavor " + BuildConfig.FLAVOR_net);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,18 +14,18 @@ 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 -> {
|
||||
XMR("XMR", true, "monero:tx_amount:recipient_name:tx_description", 0, R.drawable.ic_monero, R.drawable.ic_monero_bw, Wallet::isAddressValid),
|
||||
BTC("BTC", true, "bitcoin:amount:label:message", 0, 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 -> {
|
||||
DASH("DASH", true, "dash:amount:label:message", 0, 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 -> {
|
||||
DOGE("DOGE", true, "dogecoin:amount:label:message", 0, 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 -> {
|
||||
ETH("ETH", false, "ethereum:amount:label:message", 0, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, EthAddressValidator::validate),
|
||||
LTC("LTC", true, "litecoin:amount:label:message", 0, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, address -> {
|
||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC);
|
||||
});
|
||||
|
||||
|
@ -16,9 +16,6 @@
|
||||
|
||||
package com.m2049r.xmrwallet.data;
|
||||
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -59,22 +56,6 @@ public class UserNotes {
|
||||
txNotes = buildTxNote();
|
||||
}
|
||||
|
||||
public void setXmrtoOrder(CreateOrder order) {
|
||||
if (order != null) {
|
||||
xmrtoTag = order.TAG;
|
||||
xmrtoKey = order.getOrderId();
|
||||
xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount());
|
||||
xmrtoCurrency = order.getBtcCurrency();
|
||||
xmrtoDestination = order.getBtcAddress();
|
||||
} else {
|
||||
xmrtoTag = null;
|
||||
xmrtoKey = null;
|
||||
xmrtoAmount = null;
|
||||
xmrtoDestination = null;
|
||||
}
|
||||
txNotes = buildTxNote();
|
||||
}
|
||||
|
||||
private String buildTxNote() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (xmrtoKey != null) {
|
||||
|
@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.m2049r.xmrwallet.BuildConfig;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class AboutFragment extends DialogFragment {
|
||||
static final String TAG = "AboutFragment";
|
||||
|
||||
public static AboutFragment newInstance() {
|
||||
return new AboutFragment();
|
||||
}
|
||||
|
||||
public static void display(FragmentManager fm) {
|
||||
FragmentTransaction ft = fm.beginTransaction();
|
||||
Fragment prev = fm.findFragmentByTag(TAG);
|
||||
if (prev != null) {
|
||||
ft.remove(prev);
|
||||
}
|
||||
|
||||
AboutFragment.newInstance().show(ft, TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View view = LayoutInflater.from(getActivity()).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())
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.about_close,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
private String getLicencesHtml() {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(getContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null)
|
||||
sb.append(line);
|
||||
return sb.toString();
|
||||
} catch (IOException ex) {
|
||||
Timber.e(ex);
|
||||
return ex.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.dialog;
|
||||
|
||||
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.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
public class CreditsFragment extends DialogFragment {
|
||||
static final String TAG = "DonationFragment";
|
||||
|
||||
public static CreditsFragment newInstance() {
|
||||
return new CreditsFragment();
|
||||
}
|
||||
|
||||
public static void display(FragmentManager fm) {
|
||||
FragmentTransaction ft = fm.beginTransaction();
|
||||
Fragment prev = fm.findFragmentByTag(TAG);
|
||||
if (prev != null) {
|
||||
ft.remove(prev);
|
||||
}
|
||||
|
||||
CreditsFragment.newInstance().show(ft, TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View view = LayoutInflater.from(getActivity()).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())
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.about_close,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.dialog;
|
||||
|
||||
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;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
public class HelpFragment extends DialogFragment {
|
||||
static final String TAG = "HelpFragment";
|
||||
private static final String HELP_ID = "HELP_ID";
|
||||
private static final String TOR_BUTTON = "TOR";
|
||||
|
||||
public static HelpFragment newInstance(int helpResourceId) {
|
||||
HelpFragment fragment = new HelpFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(HELP_ID, helpResourceId);
|
||||
// a hack for the tor button
|
||||
if (helpResourceId == R.string.help_tor)
|
||||
bundle.putInt(TOR_BUTTON, 7);
|
||||
fragment.setArguments(bundle);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static void display(FragmentManager fm, int helpResourceId) {
|
||||
FragmentTransaction ft = fm.beginTransaction();
|
||||
Fragment prev = fm.findFragmentByTag(TAG);
|
||||
if (prev != null) {
|
||||
ft.remove(prev);
|
||||
}
|
||||
|
||||
HelpFragment.newInstance(helpResourceId).show(ft, TAG);
|
||||
}
|
||||
|
||||
private Spanned getHtml(String html, double textSize) {
|
||||
final Html.ImageGetter imageGetter = source -> {
|
||||
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);
|
||||
final double f = textSize / drawable.getIntrinsicHeight();
|
||||
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
|
||||
return drawable;
|
||||
};
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY, imageGetter, null);
|
||||
} else {
|
||||
return Html.fromHtml(html, imageGetter, null);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_help, null);
|
||||
|
||||
int helpId = 0;
|
||||
boolean torButton = false;
|
||||
Bundle arguments = getArguments();
|
||||
if (arguments != null) {
|
||||
helpId = arguments.getInt(HELP_ID);
|
||||
torButton = arguments.getInt(TOR_BUTTON) > 0;
|
||||
}
|
||||
final TextView helpTv = view.findViewById(R.id.tvHelp);
|
||||
if (helpId > 0)
|
||||
helpTv.setText(getHtml(getString(helpId), helpTv.getTextSize()));
|
||||
|
||||
MaterialAlertDialogBuilder builder =
|
||||
new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(view);
|
||||
if (torButton) {
|
||||
builder.setNegativeButton(R.string.help_nok,
|
||||
(dialog, id) -> dialog.dismiss())
|
||||
.setPositiveButton(R.string.help_getorbot,
|
||||
(dialog, id) -> {
|
||||
dialog.dismiss();
|
||||
NetCipherHelper.getInstance().installOrbot(requireActivity());
|
||||
});
|
||||
} else {
|
||||
builder.setNegativeButton(R.string.help_ok,
|
||||
(dialog, id) -> dialog.dismiss());
|
||||
}
|
||||
return builder.create();
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.dialog;
|
||||
|
||||
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.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
public class PrivacyFragment extends DialogFragment {
|
||||
static final String TAG = "PrivacyFragment";
|
||||
|
||||
public static PrivacyFragment newInstance() {
|
||||
return new PrivacyFragment();
|
||||
}
|
||||
|
||||
public static void display(FragmentManager fm) {
|
||||
FragmentTransaction ft = fm.beginTransaction();
|
||||
Fragment prev = fm.findFragmentByTag(TAG);
|
||||
if (prev != null) {
|
||||
ft.remove(prev);
|
||||
}
|
||||
|
||||
PrivacyFragment.newInstance().show(ft, TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final View view = LayoutInflater.from(getActivity()).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())
|
||||
.setView(view)
|
||||
.setNegativeButton(R.string.about_close,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package com.m2049r.xmrwallet.dialog;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
* Copyright (C) 2018 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.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ProgressDialog extends AlertDialog {
|
||||
|
||||
private ProgressBar pbBar;
|
||||
|
||||
private TextView tvMessage;
|
||||
|
||||
private TextView tvProgress;
|
||||
|
||||
private View rlProgressBar, pbCircle;
|
||||
|
||||
static private final String PROGRESS_FORMAT = "%1d/%2d";
|
||||
|
||||
private CharSequence message;
|
||||
private int maxValue, progressValue;
|
||||
private boolean indeterminate = true;
|
||||
|
||||
public ProgressDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
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);
|
||||
pbBar = view.findViewById(R.id.pbBar);
|
||||
tvProgress = view.findViewById(R.id.tvProgress);
|
||||
setView(view);
|
||||
setIndeterminate(indeterminate);
|
||||
if (maxValue > 0) {
|
||||
setMax(maxValue);
|
||||
}
|
||||
if (progressValue > 0) {
|
||||
setProgress(progressValue);
|
||||
}
|
||||
if (message != null) {
|
||||
Timber.d("msg=%s", message);
|
||||
setMessage(message);
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Helper.preventScreenshot()) {
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgress(int value, int max) {
|
||||
progressValue = value;
|
||||
maxValue = max;
|
||||
if (pbBar != null) {
|
||||
pbBar.setProgress(value);
|
||||
pbBar.setMax(max);
|
||||
tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue));
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgress(int value) {
|
||||
progressValue = value;
|
||||
if (pbBar != null) {
|
||||
pbBar.setProgress(value);
|
||||
tvProgress.setText(String.format(Locale.getDefault(), PROGRESS_FORMAT, value, maxValue));
|
||||
}
|
||||
}
|
||||
|
||||
public void setMax(int max) {
|
||||
maxValue = max;
|
||||
if (pbBar != null) {
|
||||
pbBar.setMax(max);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIndeterminate(boolean indeterminate) {
|
||||
if (this.indeterminate != indeterminate) {
|
||||
if (rlProgressBar != null) {
|
||||
if (indeterminate) {
|
||||
pbCircle.setVisibility(View.VISIBLE);
|
||||
rlProgressBar.setVisibility(View.GONE);
|
||||
} else {
|
||||
pbCircle.setVisibility(View.GONE);
|
||||
rlProgressBar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
this.indeterminate = indeterminate;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessage(CharSequence message) {
|
||||
this.message = message;
|
||||
if (tvMessage != null) {
|
||||
tvMessage.setText(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
package com.m2049r.xmrwallet.fragment.home;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class HomeFragment extends Fragment {
|
||||
|
||||
private HomeViewModel mViewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_home, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
|
||||
MainActivity mainActivity = (MainActivity) getActivity();
|
||||
if(mainActivity == null) return;
|
||||
|
||||
ImageView settingsImageView = view.findViewById(R.id.settings_imageview);
|
||||
ImageView addressImageView = view.findViewById(R.id.monero_qr_imageview);
|
||||
TextView addressTextView = view.findViewById(R.id.address_textview);
|
||||
TextView balanceTextView = view.findViewById(R.id.balance_textview);
|
||||
EditText addressEditText = view.findViewById(R.id.address_edittext);
|
||||
EditText amountEditText = view.findViewById(R.id.amount_edittext);
|
||||
Button sendButton = view.findViewById(R.id.send_button);
|
||||
|
||||
mainActivity.address.observe(getViewLifecycleOwner(), addr -> {
|
||||
if(!addr.isEmpty()) {
|
||||
addressTextView.setText(addr);
|
||||
addressImageView.setImageBitmap(mViewModel.generate(addr, 256, 256));
|
||||
}
|
||||
});
|
||||
|
||||
mainActivity.balance.observe(getViewLifecycleOwner(), balance -> {
|
||||
balanceTextView.setText(getString(R.string.wallet_balance_text, Wallet.getDisplayAmount(balance)));
|
||||
});
|
||||
|
||||
TxService.getInstance().clearSendEvent.observe(getViewLifecycleOwner(), o -> {
|
||||
addressEditText.setText(null);
|
||||
amountEditText.setText(null);
|
||||
sendButton.setEnabled(true);
|
||||
});
|
||||
|
||||
settingsImageView.setOnClickListener(view12 -> {
|
||||
navigate(R.id.settings_fragment);
|
||||
});
|
||||
|
||||
sendButton.setOnClickListener(view1 -> {
|
||||
String address = addressEditText.getText().toString().trim();
|
||||
String amount = amountEditText.getText().toString().trim();
|
||||
boolean validAddress = Wallet.isAddressValid(address);
|
||||
if(validAddress && !amount.isEmpty()) {
|
||||
sendButton.setEnabled(false);
|
||||
TxService.getInstance().sendTx(address, amount);
|
||||
} else if(!validAddress) {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
|
||||
} else if(amount.isEmpty()) {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_amount_empty), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void navigate(int destination) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if(activity != null) {
|
||||
FragmentManager fm = activity.getSupportFragmentManager();
|
||||
NavHostFragment navHostFragment =
|
||||
(NavHostFragment) fm.findFragmentById(R.id.nav_host_fragment);
|
||||
if(navHostFragment != null) {
|
||||
navHostFragment.getNavController().navigate(destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package com.m2049r.xmrwallet.fragment.home;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class HomeViewModel extends ViewModel {
|
||||
public Bitmap generate(String text, int width, int height) {
|
||||
if ((width <= 0) || (height <= 0)) return null;
|
||||
Map<EncodeHintType, Object> hints = new HashMap<>();
|
||||
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
|
||||
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
|
||||
try {
|
||||
BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
|
||||
int[] pixels = new int[width * height];
|
||||
for (int i = 0; i < height; i++) {
|
||||
for (int j = 0; j < width; j++) {
|
||||
if (bitMatrix.get(j, i)) {
|
||||
pixels[i * width + j] = 0x00000000;
|
||||
} else {
|
||||
pixels[i * height + j] = 0xffffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.RGB_565);
|
||||
} catch (WriterException ex) {
|
||||
Timber.e(ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,525 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import android.content.Context;
|
||||
import android.nfc.NfcManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Patterns;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
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.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;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
|
||||
static final int INTEGRATED_ADDRESS_LENGTH = 106;
|
||||
|
||||
public static SendAddressWizardFragment newInstance(Listener listener) {
|
||||
SendAddressWizardFragment instance = new SendAddressWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
|
||||
public void setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void setBarcodeData(BarcodeData data);
|
||||
|
||||
BarcodeData getBarcodeData();
|
||||
|
||||
BarcodeData popBarcodeData();
|
||||
|
||||
void setMode(SendFragment.Mode mode);
|
||||
|
||||
TxData getTxData();
|
||||
}
|
||||
|
||||
private EditText etDummy;
|
||||
private TextInputLayout etAddress;
|
||||
private TextInputLayout etNotes;
|
||||
private TextView tvXmrTo;
|
||||
private TextView tvTor;
|
||||
private Map<Crypto, ImageButton> ibCrypto;
|
||||
final private Set<Crypto> possibleCryptos = new HashSet<>();
|
||||
private Crypto selectedCrypto = null;
|
||||
|
||||
private boolean resolvingOA = false;
|
||||
|
||||
OnScanListener onScanListener;
|
||||
|
||||
public interface OnScanListener {
|
||||
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) {
|
||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_send_address, container, false);
|
||||
|
||||
tvTor = view.findViewById(R.id.tvTor);
|
||||
tvXmrTo = view.findViewById(R.id.tvXmrTo);
|
||||
ibCrypto = new HashMap<>();
|
||||
for (Crypto crypto : Crypto.values()) {
|
||||
final ImageButton button = view.findViewById(crypto.getButtonId());
|
||||
if (Helper.ALLOW_SHIFT || (crypto == Crypto.XMR)) {
|
||||
ibCrypto.put(crypto, button);
|
||||
button.setOnClickListener(v -> {
|
||||
if (possibleCryptos.contains(crypto)) {
|
||||
selectedCrypto = crypto;
|
||||
updateCryptoButtons(false);
|
||||
} 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.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
|
||||
tvXmrTo.setVisibility(View.VISIBLE);
|
||||
tvTor.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
button.setImageResource(crypto.getIconDisabledId());
|
||||
button.setImageAlpha(128);
|
||||
button.setEnabled(false);
|
||||
}
|
||||
}
|
||||
if (!Helper.ALLOW_SHIFT) {
|
||||
tvTor.setVisibility(View.VISIBLE);
|
||||
}
|
||||
updateCryptoButtons(true);
|
||||
|
||||
etAddress = view.findViewById(R.id.etAddress);
|
||||
etAddress.getEditText().setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
etAddress.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
// ignore ENTER
|
||||
return ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER));
|
||||
}
|
||||
});
|
||||
etAddress.getEditText().setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (!hasFocus) {
|
||||
String enteredAddress = etAddress.getEditText().getText().toString().trim();
|
||||
String dnsOA = dnsFromOpenAlias(enteredAddress);
|
||||
Timber.d("OpenAlias is %s", dnsOA);
|
||||
if (dnsOA != null) {
|
||||
processOpenAlias(dnsOA);
|
||||
}
|
||||
}
|
||||
});
|
||||
etAddress.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
Timber.d("AFTER: %s", editable.toString());
|
||||
etAddress.setError(null);
|
||||
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 (possibleCryptos.isEmpty()) {
|
||||
Timber.d("other");
|
||||
tvXmrTo.setVisibility(View.INVISIBLE);
|
||||
sendListener.setMode(SendFragment.Mode.XMR);
|
||||
}
|
||||
updateCryptoButtons(address.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
|
||||
bPasteAddress.setOnClickListener(v -> {
|
||||
final String clip = Helper.getClipBoardText(getActivity());
|
||||
if (clip == null) return;
|
||||
// clean it up
|
||||
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
|
||||
BarcodeData bc = BarcodeData.fromString(address);
|
||||
if (bc != null) {
|
||||
processScannedData(bc);
|
||||
final EditText et = etAddress.getEditText();
|
||||
et.setSelection(et.getText().length());
|
||||
etAddress.requestFocus();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
final View cvScan = view.findViewById(R.id.bScan);
|
||||
cvScan.setOnClickListener(v -> onScanListener.onScan());
|
||||
|
||||
etDummy = view.findViewById(R.id.etDummy);
|
||||
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
etDummy.requestFocus();
|
||||
|
||||
View tvNfc = view.findViewById(R.id.tvNfc);
|
||||
NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE);
|
||||
if ((manager != null) && (manager.getDefaultAdapter() != null))
|
||||
tvNfc.setVisibility(View.VISIBLE);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void selectedCrypto(Crypto crypto) {
|
||||
final ImageButton button = ibCrypto.get(crypto);
|
||||
button.setImageResource(crypto.getIconEnabledId());
|
||||
button.setImageAlpha(255);
|
||||
button.setEnabled(true);
|
||||
}
|
||||
|
||||
private void possibleCrypto(Crypto crypto) {
|
||||
final ImageButton button = ibCrypto.get(crypto);
|
||||
button.setImageResource(crypto.getIconDisabledId());
|
||||
button.setImageAlpha(255);
|
||||
button.setEnabled(true);
|
||||
}
|
||||
|
||||
private void impossibleCrypto(Crypto crypto) {
|
||||
final ImageButton button = ibCrypto.get(crypto);
|
||||
button.setImageResource(crypto.getIconDisabledId());
|
||||
button.setImageAlpha(128);
|
||||
button.setEnabled(true);
|
||||
}
|
||||
|
||||
private void updateCryptoButtons(boolean noAddress) {
|
||||
if (!Helper.ALLOW_SHIFT) return;
|
||||
for (Crypto crypto : Crypto.values()) {
|
||||
if (crypto == selectedCrypto) {
|
||||
selectedCrypto(crypto);
|
||||
} else if (possibleCryptos.contains(crypto)) {
|
||||
possibleCrypto(crypto);
|
||||
} else {
|
||||
impossibleCrypto(crypto);
|
||||
}
|
||||
}
|
||||
if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) {
|
||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getSymbol())));
|
||||
tvXmrTo.setVisibility(View.VISIBLE);
|
||||
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
|
||||
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous)));
|
||||
tvXmrTo.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvXmrTo.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if (noAddress) {
|
||||
selectedCrypto(Crypto.XMR);
|
||||
}
|
||||
}
|
||||
|
||||
private void processOpenAlias(String dnsOA) {
|
||||
if (resolvingOA) return; // already resolving - just wait
|
||||
sendListener.popBarcodeData();
|
||||
if (dnsOA != null) {
|
||||
resolvingOA = true;
|
||||
etAddress.setError(getString(R.string.send_address_resolve_openalias));
|
||||
OpenAliasHelper.resolve(dnsOA, new OpenAliasHelper.OnResolvedListener() {
|
||||
@Override
|
||||
public void onResolved(Map<Crypto, BarcodeData> dataMap) {
|
||||
resolvingOA = false;
|
||||
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);
|
||||
processScannedData(barcodeData);
|
||||
} else {
|
||||
etAddress.setError(getString(R.string.send_address_not_openalias));
|
||||
Timber.d("NO XMR OPENALIAS TXT FOUND");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
resolvingOA = false;
|
||||
etAddress.setError(getString(R.string.send_address_not_openalias));
|
||||
Timber.e("OA FAILED");
|
||||
}
|
||||
});
|
||||
} // else ignore
|
||||
}
|
||||
|
||||
private boolean checkAddressNoError() {
|
||||
return selectedCrypto != null;
|
||||
}
|
||||
|
||||
private boolean checkAddress() {
|
||||
boolean ok = checkAddressNoError();
|
||||
if (possibleCryptos.isEmpty()) {
|
||||
etAddress.setError(getString(R.string.send_address_invalid));
|
||||
} else {
|
||||
etAddress.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean isStandardAddress(String address) {
|
||||
return Wallet.isAddressValid(address);
|
||||
}
|
||||
|
||||
private boolean isIntegratedAddress(String address) {
|
||||
return (address.length() == INTEGRATED_ADDRESS_LENGTH)
|
||||
&& 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()) {
|
||||
if (possibleCryptos.contains(crypto)) {
|
||||
ibCrypto.get(crypto).startAnimation(Helper.getShakeAnimation(getContext()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
etAddress.startAnimation(Helper.getShakeAnimation(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
if (!checkAddressNoError()) {
|
||||
shakeAddress();
|
||||
String enteredAddress = etAddress.getEditText().getText().toString().trim();
|
||||
String dnsOA = dnsFromOpenAlias(enteredAddress);
|
||||
Timber.d("OpenAlias is %s", dnsOA);
|
||||
if (dnsOA != null) {
|
||||
processOpenAlias(dnsOA);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sendListener != null) {
|
||||
TxData txData = sendListener.getTxData();
|
||||
if (txData instanceof TxDataBtc) {
|
||||
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
|
||||
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
|
||||
txData.setDestinationAddress(null);
|
||||
ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase();
|
||||
} else {
|
||||
txData.setDestinationAddress(etAddress.getEditText().getText().toString());
|
||||
ServiceHelper.ASSET = null;
|
||||
}
|
||||
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
|
||||
txData.setPriority(PendingTransaction.Priority.Priority_Default);
|
||||
txData.setMixin(SendFragment.MIXIN);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
if (context instanceof OnScanListener) {
|
||||
onScanListener = (OnScanListener) context;
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement ScanListener");
|
||||
}
|
||||
}
|
||||
|
||||
// QR Scan Stuff
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume");
|
||||
processScannedData();
|
||||
}
|
||||
|
||||
public void processScannedData(BarcodeData barcodeData) {
|
||||
sendListener.setBarcodeData(barcodeData);
|
||||
if (isResumed())
|
||||
processScannedData();
|
||||
}
|
||||
|
||||
public void processScannedData() {
|
||||
BarcodeData barcodeData = sendListener.getBarcodeData();
|
||||
if (barcodeData != null) {
|
||||
Timber.d("GOT DATA");
|
||||
if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) {
|
||||
Timber.d("BUT ONLY XMR SUPPORTED");
|
||||
barcodeData = null;
|
||||
sendListener.setBarcodeData(barcodeData);
|
||||
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));
|
||||
}
|
||||
} else {
|
||||
etAddress.getEditText().getText().clear();
|
||||
etAddress.setError(null);
|
||||
}
|
||||
|
||||
String scannedNotes = barcodeData.addressName;
|
||||
if (scannedNotes == null) {
|
||||
scannedNotes = barcodeData.description;
|
||||
} else if (barcodeData.description != null) {
|
||||
scannedNotes = scannedNotes + ": " + barcodeData.description;
|
||||
}
|
||||
if (scannedNotes != null) {
|
||||
etNotes.getEditText().setText(scannedNotes);
|
||||
} else {
|
||||
etNotes.getEditText().getText().clear();
|
||||
etNotes.setError(null);
|
||||
}
|
||||
} else
|
||||
Timber.d("barcodeData=null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
etDummy.requestFocus();
|
||||
}
|
||||
|
||||
String dnsFromOpenAlias(String openalias) {
|
||||
Timber.d("checking openalias candidate %s", openalias);
|
||||
if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias;
|
||||
if (Patterns.EMAIL_ADDRESS.matcher(openalias).matches()) {
|
||||
openalias = openalias.replaceFirst("@", ".");
|
||||
if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias;
|
||||
}
|
||||
return null; // not an openalias
|
||||
}
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.widget.ExchangeEditText;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
|
||||
public static SendAmountWizardFragment newInstance(Listener listener) {
|
||||
SendAmountWizardFragment instance = new SendAmountWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
|
||||
public void setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
SendFragment.Listener getActivityCallback();
|
||||
|
||||
TxData getTxData();
|
||||
|
||||
BarcodeData popBarcodeData();
|
||||
}
|
||||
|
||||
private TextView tvFunds;
|
||||
private ExchangeEditText etAmount;
|
||||
private View rlSweep;
|
||||
private ImageButton ibSweep;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||
|
||||
sendListener = (Listener) getParentFragment();
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_send_amount, container, false);
|
||||
|
||||
tvFunds = view.findViewById(R.id.tvFunds);
|
||||
etAmount = view.findViewById(R.id.etAmount);
|
||||
rlSweep = view.findViewById(R.id.rlSweep);
|
||||
|
||||
view.findViewById(R.id.ivSweep).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sweepAll(false);
|
||||
}
|
||||
});
|
||||
|
||||
ibSweep = view.findViewById(R.id.ibSweep);
|
||||
|
||||
ibSweep.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sweepAll(true);
|
||||
}
|
||||
});
|
||||
|
||||
etAmount.requestFocus();
|
||||
return view;
|
||||
}
|
||||
|
||||
private boolean spendAllMode = false;
|
||||
|
||||
private void sweepAll(boolean spendAllMode) {
|
||||
if (spendAllMode) {
|
||||
ibSweep.setVisibility(View.INVISIBLE);
|
||||
etAmount.setVisibility(View.GONE);
|
||||
rlSweep.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
ibSweep.setVisibility(View.VISIBLE);
|
||||
etAmount.setVisibility(View.VISIBLE);
|
||||
rlSweep.setVisibility(View.GONE);
|
||||
}
|
||||
this.spendAllMode = spendAllMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
if (spendAllMode) {
|
||||
if (sendListener != null) {
|
||||
sendListener.getTxData().setAmount(Wallet.SWEEP_ALL);
|
||||
}
|
||||
} else {
|
||||
if (!etAmount.validate(maxFunds, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sendListener != null) {
|
||||
String xmr = etAmount.getNativeAmount();
|
||||
if (xmr != null) {
|
||||
sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr));
|
||||
} else {
|
||||
sendListener.getTxData().setAmount(0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double maxFunds = 0;
|
||||
|
||||
@Override
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.showKeyboard(getActivity());
|
||||
final long funds = getTotalFunds();
|
||||
maxFunds = 1.0 * funds / Helper.ONE_XMR;
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
tvFunds.setText(getString(R.string.send_available,
|
||||
Wallet.getDisplayAmount(funds)));
|
||||
} else {
|
||||
tvFunds.setText(getString(R.string.send_available,
|
||||
getString(R.string.unknown_amount)));
|
||||
}
|
||||
final BarcodeData data = sendListener.popBarcodeData();
|
||||
if ((data != null) && (data.amount != null)) {
|
||||
etAmount.setAmount(data.amount);
|
||||
}
|
||||
}
|
||||
|
||||
long getTotalFunds() {
|
||||
return sendListener.getActivityCallback().getTotalFunds();
|
||||
}
|
||||
}
|
@ -1,263 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
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.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 static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) {
|
||||
SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
}
|
||||
|
||||
SendAmountWizardFragment.Listener sendListener;
|
||||
|
||||
public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) {
|
||||
this.sendListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
private TextView tvFunds;
|
||||
private ExchangeOtherEditText etAmount;
|
||||
|
||||
private TextView tvXmrToParms;
|
||||
private SendProgressView evParams;
|
||||
private View llXmrToParms;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
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);
|
||||
|
||||
evParams = view.findViewById(R.id.evXmrToParms);
|
||||
llXmrToParms = view.findViewById(R.id.llXmrToParms);
|
||||
|
||||
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
|
||||
|
||||
etAmount = view.findViewById(R.id.etAmount);
|
||||
etAmount.requestFocus();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
Timber.i(maxBtc + "/" + minBtc);
|
||||
if (!etAmount.validate(maxBtc, minBtc)) {
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double maxBtc = 0;
|
||||
double minBtc = 0;
|
||||
|
||||
@Override
|
||||
public void onPauseFragment() {
|
||||
llXmrToParms.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResumeFragment() {
|
||||
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!");
|
||||
final long funds = getTotalFunds();
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
tvFunds.setText(getString(R.string.send_available,
|
||||
Wallet.getDisplayAmount(funds)));
|
||||
//TODO
|
||||
} else {
|
||||
tvFunds.setText(getString(R.string.send_available,
|
||||
getString(R.string.unknown_amount)));
|
||||
}
|
||||
etAmount.setAmount("");
|
||||
final BarcodeData data = sendListener.popBarcodeData();
|
||||
if (data != null) {
|
||||
if (data.amount != null) {
|
||||
etAmount.setAmount(data.amount);
|
||||
}
|
||||
}
|
||||
etAmount.setBaseCurrency(btcSymbol);
|
||||
callXmrTo();
|
||||
}
|
||||
|
||||
long getTotalFunds() {
|
||||
return sendListener.getActivityCallback().getTotalFunds();
|
||||
}
|
||||
|
||||
private QueryOrderParameters orderParameters = null;
|
||||
|
||||
private void processOrderParms(final QueryOrderParameters orderParameters) {
|
||||
this.orderParameters = orderParameters;
|
||||
getView().post(() -> {
|
||||
final double price = orderParameters.getPrice();
|
||||
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 TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
|
||||
min, max, rate, txDataBtc.getBtcSymbol()));
|
||||
tvXmrToParms.setText(xmrParmText);
|
||||
|
||||
final long funds = getTotalFunds();
|
||||
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);
|
||||
} else {
|
||||
availBtcString = getString(R.string.unknown_amount);
|
||||
availXmrString = availBtcString;
|
||||
}
|
||||
tvFunds.setText(getString(R.string.send_available_btc,
|
||||
availXmrString,
|
||||
availBtcString,
|
||||
((TxDataBtc) sendListener.getTxData()).getBtcSymbol()));
|
||||
llXmrToParms.setVisibility(View.VISIBLE);
|
||||
evParams.hideProgress();
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void callXmrTo() {
|
||||
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;
|
||||
|
||||
private SideShiftApi getXmrToApi() {
|
||||
if (xmrToApi == null) {
|
||||
synchronized (this) {
|
||||
if (xmrToApi == null) {
|
||||
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
return xmrToApi;
|
||||
}
|
||||
}
|
@ -1,551 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
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.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 static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) {
|
||||
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
}
|
||||
|
||||
SendConfirmWizardFragment.Listener sendListener;
|
||||
|
||||
public void setSendListener(SendConfirmWizardFragment.Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
|
||||
private View llStageA;
|
||||
private SendProgressView evStageA;
|
||||
private View llStageB;
|
||||
private SendProgressView evStageB;
|
||||
private View llStageC;
|
||||
private SendProgressView evStageC;
|
||||
private TextView tvTxBtcAmount;
|
||||
private TextView tvTxBtcRate;
|
||||
private TextView tvTxBtcAddress;
|
||||
private TextView tvTxBtcAddressLabel;
|
||||
private TextView tvTxXmrToKey;
|
||||
private TextView tvTxFee;
|
||||
private TextView tvTxTotal;
|
||||
private View llConfirmSend;
|
||||
private Button bSend;
|
||||
private View pbProgressSend;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
|
||||
|
||||
View view = inflater.inflate(
|
||||
R.layout.fragment_send_btc_confirm, container, false);
|
||||
|
||||
tvTxBtcAddress = view.findViewById(R.id.tvTxBtcAddress);
|
||||
tvTxBtcAddressLabel = view.findViewById(R.id.tvTxBtcAddressLabel);
|
||||
tvTxBtcAmount = view.findViewById(R.id.tvTxBtcAmount);
|
||||
tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate);
|
||||
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
||||
|
||||
tvTxFee = view.findViewById(R.id.tvTxFee);
|
||||
tvTxTotal = view.findViewById(R.id.tvTxTotal);
|
||||
|
||||
llStageA = view.findViewById(R.id.llStageA);
|
||||
evStageA = view.findViewById(R.id.evStageA);
|
||||
llStageB = view.findViewById(R.id.llStageB);
|
||||
evStageB = view.findViewById(R.id.evStageB);
|
||||
llStageC = view.findViewById(R.id.llStageC);
|
||||
evStageC = view.findViewById(R.id.evStageC);
|
||||
|
||||
tvTxXmrToKey.setOnClickListener(v -> {
|
||||
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
|
||||
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
llConfirmSend = view.findViewById(R.id.llConfirmSend);
|
||||
pbProgressSend = view.findViewById(R.id.pbProgressSend);
|
||||
|
||||
bSend = view.findViewById(R.id.bSend);
|
||||
bSend.setEnabled(false);
|
||||
|
||||
bSend.setOnClickListener(v -> {
|
||||
Timber.d("bSend.setOnClickListener");
|
||||
bSend.setEnabled(false);
|
||||
preSend();
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideProgress() {
|
||||
Timber.d("hideProgress(%d)", inProgress);
|
||||
switch (inProgress) {
|
||||
case STAGE_A:
|
||||
evStageA.hideProgress();
|
||||
llStageA.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case STAGE_B:
|
||||
evStageB.hideProgress();
|
||||
llStageB.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case STAGE_C:
|
||||
evStageC.hideProgress();
|
||||
llStageC.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unknown stage " + inProgress);
|
||||
}
|
||||
inProgress = STAGE_X;
|
||||
}
|
||||
|
||||
public void showStageError(String code, String message, String solution) {
|
||||
switch (inProgress) {
|
||||
case STAGE_A:
|
||||
evStageA.showMessage(code, message, solution);
|
||||
break;
|
||||
case STAGE_B:
|
||||
evStageB.showMessage(code, message, solution);
|
||||
break;
|
||||
case STAGE_C:
|
||||
evStageC.showMessage(code, message, solution);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unknown stage");
|
||||
}
|
||||
inProgress = STAGE_X;
|
||||
}
|
||||
|
||||
PendingTransaction pendingTransaction = null;
|
||||
|
||||
void send() {
|
||||
Timber.d("SEND @%d", sendCountdown);
|
||||
if (sendCountdown <= 0) {
|
||||
Timber.i("User waited too long in password dialog.");
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendFailed(String error) {
|
||||
pbProgressSend.setVisibility(View.INVISIBLE);
|
||||
Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@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))) {
|
||||
this.pendingTransaction = pendingTransaction;
|
||||
getView().post(() -> {
|
||||
hideProgress();
|
||||
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
|
||||
tvTxTotal.setText(Wallet.getDisplayAmount(
|
||||
pendingTransaction.getFee() + pendingTransaction.getAmount()));
|
||||
updateSendButton();
|
||||
});
|
||||
} else {
|
||||
this.pendingTransaction = null;
|
||||
sendListener.disposeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createTransactionFailed(String errorText) {
|
||||
Timber.e("CREATE TX FAILED");
|
||||
if (pendingTransaction != null) {
|
||||
throw new IllegalStateException("pendingTransaction is not null");
|
||||
}
|
||||
showStageError(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;
|
||||
stopSendTimer();
|
||||
sendListener.disposeTransaction();
|
||||
pendingTransaction = null;
|
||||
inProgress = STAGE_X;
|
||||
updateSendButton();
|
||||
super.onPauseFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
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!");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
llStageA.setVisibility(View.INVISIBLE);
|
||||
evStageA.hideProgress();
|
||||
llStageB.setVisibility(View.INVISIBLE);
|
||||
evStageB.hideProgress();
|
||||
llStageC.setVisibility(View.INVISIBLE);
|
||||
evStageC.hideProgress();
|
||||
isResumed = true;
|
||||
if ((pendingTransaction == null) && (inProgress == STAGE_X)) {
|
||||
stageA();
|
||||
} // otherwise just sit there blank
|
||||
// TODO: don't sit there blank - can this happen? should we just die?
|
||||
}
|
||||
|
||||
private int sendCountdown = 0;
|
||||
private static final int XMRTO_COUNTDOWN_STEP = 1; // 1 second
|
||||
|
||||
Runnable updateRunnable = null;
|
||||
|
||||
void startSendTimer(int timeout) {
|
||||
Timber.d("startSendTimer()");
|
||||
sendCountdown = timeout;
|
||||
updateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isAdded())
|
||||
return;
|
||||
Timber.d("updateTimer()");
|
||||
if (sendCountdown <= 0) {
|
||||
bSend.setEnabled(false);
|
||||
sendCountdown = 0;
|
||||
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
int minutes = sendCountdown / 60;
|
||||
int seconds = sendCountdown % 60;
|
||||
String t = String.format("%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);
|
||||
}
|
||||
}
|
||||
};
|
||||
getView().post(updateRunnable);
|
||||
}
|
||||
|
||||
void stopSendTimer() {
|
||||
getView().removeCallbacks(updateRunnable);
|
||||
}
|
||||
|
||||
void updateSendButton() {
|
||||
Timber.d("updateSendButton()");
|
||||
if (pendingTransaction != null) {
|
||||
llConfirmSend.setVisibility(View.VISIBLE);
|
||||
bSend.setEnabled(sendCountdown > 0);
|
||||
} else {
|
||||
llConfirmSend.setVisibility(View.GONE);
|
||||
bSend.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void preSend() {
|
||||
Helper.promptPassword(getContext(), getActivityCallback().getWalletName(), false, new Helper.PasswordAction() {
|
||||
@Override
|
||||
public void act(String walletName, String password, boolean fingerprintUsed) {
|
||||
send();
|
||||
}
|
||||
|
||||
public void fail(String walletName) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
bSend.setEnabled(sendCountdown > 0); // allow to try again
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// creates a pending transaction and calls us back with transactionCreated()
|
||||
// or createTransactionFailed()
|
||||
void prepareSend() {
|
||||
if (!isResumed) return;
|
||||
if ((xmrtoOrder == null)) {
|
||||
throw new IllegalStateException("xmrtoOrder is null");
|
||||
}
|
||||
showProgress(3, getString(R.string.label_send_progress_create_tx));
|
||||
final TxData txData = sendListener.getTxData();
|
||||
txData.setDestinationAddress(xmrtoOrder.getXmrAddress());
|
||||
txData.setAmount(xmrtoOrder.getXmrAmount());
|
||||
getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData);
|
||||
}
|
||||
|
||||
SendFragment.Listener getActivityCallback() {
|
||||
return sendListener.getActivityCallback();
|
||||
}
|
||||
|
||||
private RequestQuote xmrtoQuote = null;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private void processStageAError(final Exception ex) {
|
||||
Timber.e("processStageAError %s", ex.getLocalizedMessage());
|
||||
getView().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(),
|
||||
getString(R.string.text_retry));
|
||||
evStageA.setOnClickListener(v -> {
|
||||
evStageA.setOnClickListener(null);
|
||||
stageA();
|
||||
});
|
||||
} else {
|
||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
||||
getString(R.string.text_noretry));
|
||||
}
|
||||
} else {
|
||||
showStageError(getString(R.string.label_generic_xmrto_error),
|
||||
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
||||
getString(R.string.text_noretry));
|
||||
}
|
||||
} else {
|
||||
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||
ex.getLocalizedMessage(),
|
||||
getString(R.string.text_noretry));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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(() -> {
|
||||
tvTxXmrToKey.setText(order.getOrderId());
|
||||
tvTxBtcAddress.setText(order.getBtcAddress());
|
||||
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, txDataBtc.getBtcSymbol()));
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
private void processStageBError(final Exception ex) {
|
||||
Timber.e("processCreateOrderError %s", ex.getLocalizedMessage());
|
||||
getView().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(),
|
||||
getString(R.string.text_retry));
|
||||
evStageB.setOnClickListener(v -> {
|
||||
evStageB.setOnClickListener(null);
|
||||
stageB(xmrtoOrder.getOrderId());
|
||||
});
|
||||
} else {
|
||||
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
|
||||
getString(R.string.text_noretry));
|
||||
}
|
||||
} else {
|
||||
showStageError(getString(R.string.label_generic_xmrto_error),
|
||||
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
|
||||
getString(R.string.text_noretry));
|
||||
}
|
||||
} else {
|
||||
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
|
||||
ex.getLocalizedMessage(),
|
||||
getString(R.string.text_noretry));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.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;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
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.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.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SendBtcSuccessWizardFragment extends SendWizardFragment {
|
||||
|
||||
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
|
||||
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
}
|
||||
|
||||
SendSuccessWizardFragment.Listener sendListener;
|
||||
|
||||
public void setSendListener(SendSuccessWizardFragment.Listener listener) {
|
||||
this.sendListener = listener;
|
||||
}
|
||||
|
||||
ImageButton bCopyTxId;
|
||||
private TextView tvTxId;
|
||||
private TextView tvTxAddress;
|
||||
private TextView tvTxAmount;
|
||||
private TextView tvTxFee;
|
||||
private TextView tvXmrToAmount;
|
||||
private ImageView ivXmrToIcon;
|
||||
private TextView tvXmrToStatus;
|
||||
private ImageView ivXmrToStatus;
|
||||
private ImageView ivXmrToStatusBig;
|
||||
private ProgressBar pbXmrto;
|
||||
private TextView tvTxXmrToKey;
|
||||
private TextView tvXmrToSupport;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||
|
||||
View view = inflater.inflate(
|
||||
R.layout.fragment_send_btc_success, container, false);
|
||||
|
||||
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
||||
bCopyTxId.setEnabled(false);
|
||||
bCopyTxId.setOnClickListener(v -> {
|
||||
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
|
||||
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
tvXmrToAmount = view.findViewById(R.id.tvXmrToAmount);
|
||||
ivXmrToIcon = view.findViewById(R.id.ivXmrToIcon);
|
||||
tvXmrToStatus = view.findViewById(R.id.tvXmrToStatus);
|
||||
ivXmrToStatus = view.findViewById(R.id.ivXmrToStatus);
|
||||
ivXmrToStatusBig = view.findViewById(R.id.ivXmrToStatusBig);
|
||||
|
||||
tvTxId = view.findViewById(R.id.tvTxId);
|
||||
tvTxAddress = view.findViewById(R.id.tvTxAddress);
|
||||
tvTxAmount = view.findViewById(R.id.tvTxAmount);
|
||||
tvTxFee = view.findViewById(R.id.tvTxFee);
|
||||
|
||||
pbXmrto = view.findViewById(R.id.pbXmrto);
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
|
||||
tvTxXmrToKey.setOnClickListener(v -> {
|
||||
Helper.clipBoardCopy(getActivity(), 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);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isResumed = false;
|
||||
|
||||
@Override
|
||||
public void onPauseFragment() {
|
||||
isResumed = false;
|
||||
super.onPauseFragment();
|
||||
}
|
||||
|
||||
TxDataBtc btcData = null;
|
||||
|
||||
@Override
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
isResumed = true;
|
||||
|
||||
btcData = (TxDataBtc) sendListener.getTxData();
|
||||
tvTxAddress.setText(btcData.getDestinationAddress());
|
||||
|
||||
final PendingTx committedTx = sendListener.getCommittedTx();
|
||||
if (committedTx != null) {
|
||||
tvTxId.setText(committedTx.txId);
|
||||
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");
|
||||
}
|
||||
}
|
||||
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()))
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void queryOrder() {
|
||||
Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId());
|
||||
if (!isResumed) return;
|
||||
getXmrToApi().queryOrderStatus(btcData.getXmrtoOrderId(), new ShiftCallback<QueryOrderStatus>() {
|
||||
@Override
|
||||
public void onSuccess(QueryOrderStatus status) {
|
||||
if (!isAdded()) return;
|
||||
processQueryOrder(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showXmrToStatus(final QueryOrderStatus status) {
|
||||
int statusResource = 0;
|
||||
if (status.isError()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString()));
|
||||
statusResource = R.drawable.ic_error_red_24dp;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else if (status.isSent() || status.isPaid()) {
|
||||
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, btcData.getBtcSymbol()));
|
||||
statusResource = R.drawable.ic_success;
|
||||
pbXmrto.getIndeterminateDrawable().setColorFilter(
|
||||
ThemeHelper.getThemedColor(getContext(), 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),
|
||||
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),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
throw new IllegalStateException("status is broken: " + status.toString());
|
||||
}
|
||||
ivXmrToStatus.setImageResource(statusResource);
|
||||
if (status.isTerminal()) {
|
||||
pbXmrto.setVisibility(View.INVISIBLE);
|
||||
ivXmrToIcon.setVisibility(View.GONE);
|
||||
ivXmrToStatus.setVisibility(View.GONE);
|
||||
ivXmrToStatusBig.setImageResource(statusResource);
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
|
||||
interface SendConfirm {
|
||||
void sendFailed(String errorText);
|
||||
|
||||
void createTransactionFailed(String errorText);
|
||||
|
||||
void transactionCreated(String txTag, PendingTransaction pendingTransaction);
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.data.UserNotes;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SendConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
|
||||
|
||||
public static SendConfirmWizardFragment newInstance(Listener listener) {
|
||||
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
|
||||
public SendConfirmWizardFragment setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
SendFragment.Listener getActivityCallback();
|
||||
|
||||
TxData getTxData();
|
||||
|
||||
void commitTransaction();
|
||||
|
||||
void disposeTransaction();
|
||||
|
||||
SendFragment.Mode getMode();
|
||||
}
|
||||
|
||||
private TextView tvTxAddress;
|
||||
private TextView tvTxNotes;
|
||||
private TextView tvTxAmount;
|
||||
private TextView tvTxFee;
|
||||
private TextView tvTxTotal;
|
||||
private View llProgress;
|
||||
private View bSend;
|
||||
private View llConfirmSend;
|
||||
private View pbProgressSend;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||
|
||||
View view = inflater.inflate(
|
||||
R.layout.fragment_send_confirm, container, false);
|
||||
|
||||
tvTxAddress = view.findViewById(R.id.tvTxAddress);
|
||||
tvTxNotes = view.findViewById(R.id.tvTxNotes);
|
||||
tvTxAmount = view.findViewById(R.id.tvTxAmount);
|
||||
tvTxFee = view.findViewById(R.id.tvTxFee);
|
||||
tvTxTotal = view.findViewById(R.id.tvTxTotal);
|
||||
|
||||
llProgress = view.findViewById(R.id.llProgress);
|
||||
pbProgressSend = view.findViewById(R.id.pbProgressSend);
|
||||
llConfirmSend = view.findViewById(R.id.llConfirmSend);
|
||||
|
||||
bSend = view.findViewById(R.id.bSend);
|
||||
bSend.setEnabled(false);
|
||||
bSend.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Timber.d("bSend.setOnClickListener");
|
||||
bSend.setEnabled(false);
|
||||
preSend();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
boolean inProgress = false;
|
||||
|
||||
public void hideProgress() {
|
||||
llProgress.setVisibility(View.INVISIBLE);
|
||||
inProgress = false;
|
||||
}
|
||||
|
||||
public void showProgress() {
|
||||
llProgress.setVisibility(View.VISIBLE);
|
||||
inProgress = true;
|
||||
}
|
||||
|
||||
PendingTransaction pendingTransaction = null;
|
||||
|
||||
@Override
|
||||
// callback from wallet when PendingTransaction created
|
||||
public void transactionCreated(String txTag, PendingTransaction pendingTransaction) {
|
||||
// ignore txTag - the app flow ensures this is the correct tx
|
||||
// TODO: use the txTag
|
||||
hideProgress();
|
||||
if (isResumed) {
|
||||
this.pendingTransaction = pendingTransaction;
|
||||
refreshTransactionDetails();
|
||||
} else {
|
||||
sendListener.disposeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
void send() {
|
||||
sendListener.commitTransaction();
|
||||
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendFailed(String errorText) {
|
||||
pbProgressSend.setVisibility(View.INVISIBLE);
|
||||
showAlert(getString(R.string.send_create_tx_error_title), errorText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createTransactionFailed(String errorText) {
|
||||
hideProgress();
|
||||
showAlert(getString(R.string.send_create_tx_error_title), errorText);
|
||||
}
|
||||
|
||||
private void showAlert(String title, String message) {
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
builder.setCancelable(true).
|
||||
setTitle(title).
|
||||
setMessage(message).
|
||||
create().
|
||||
show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isResumed = false;
|
||||
|
||||
@Override
|
||||
public void onPauseFragment() {
|
||||
isResumed = false;
|
||||
pendingTransaction = null;
|
||||
sendListener.disposeTransaction();
|
||||
refreshTransactionDetails();
|
||||
super.onPauseFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
isResumed = true;
|
||||
|
||||
final TxData txData = sendListener.getTxData();
|
||||
tvTxAddress.setText(txData.getDestinationAddress());
|
||||
UserNotes notes = sendListener.getTxData().getUserNotes();
|
||||
if ((notes != null) && (!notes.note.isEmpty())) {
|
||||
tvTxNotes.setText(notes.note);
|
||||
} else {
|
||||
tvTxNotes.setText("-");
|
||||
}
|
||||
refreshTransactionDetails();
|
||||
if ((pendingTransaction == null) && (!inProgress)) {
|
||||
showProgress();
|
||||
prepareSend(txData);
|
||||
}
|
||||
}
|
||||
|
||||
void refreshTransactionDetails() {
|
||||
Timber.d("refreshTransactionDetails()");
|
||||
if (pendingTransaction != null) {
|
||||
llConfirmSend.setVisibility(View.VISIBLE);
|
||||
bSend.setEnabled(true);
|
||||
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
|
||||
if (getActivityCallback().isStreetMode()
|
||||
&& (sendListener.getTxData().getAmount() == Wallet.SWEEP_ALL)) {
|
||||
tvTxAmount.setText(getString(R.string.street_sweep_amount));
|
||||
tvTxTotal.setText(getString(R.string.street_sweep_amount));
|
||||
} else {
|
||||
tvTxAmount.setText(Wallet.getDisplayAmount(pendingTransaction.getAmount()));
|
||||
tvTxTotal.setText(Wallet.getDisplayAmount(
|
||||
pendingTransaction.getFee() + pendingTransaction.getAmount()));
|
||||
}
|
||||
} else {
|
||||
llConfirmSend.setVisibility(View.GONE);
|
||||
bSend.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void preSend() {
|
||||
Helper.promptPassword(getContext(), getActivityCallback().getWalletName(), false, new Helper.PasswordAction() {
|
||||
@Override
|
||||
public void act(String walletName, String password, boolean fingerprintUsed) {
|
||||
send();
|
||||
}
|
||||
|
||||
public void fail(String walletName) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
bSend.setEnabled(true); // allow to try again
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// creates a pending transaction and calls us back with transactionCreated()
|
||||
// or createTransactionFailed()
|
||||
void prepareSend(TxData txData) {
|
||||
getActivityCallback().onPrepareSend(null, txData);
|
||||
}
|
||||
|
||||
SendFragment.Listener getActivityCallback() {
|
||||
return sendListener.getActivityCallback();
|
||||
}
|
||||
}
|
@ -1,558 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.transition.MaterialContainerTransform;
|
||||
import com.m2049r.xmrwallet.OnBackPressedListener;
|
||||
import com.m2049r.xmrwallet.OnUriScannedListener;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.WalletActivity;
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.PendingTx;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.data.TxDataBtc;
|
||||
import com.m2049r.xmrwallet.data.UserNotes;
|
||||
import com.m2049r.xmrwallet.layout.SpendViewPager;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.Notice;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.widget.DotBar;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SendFragment extends Fragment
|
||||
implements SendAddressWizardFragment.Listener,
|
||||
SendAmountWizardFragment.Listener,
|
||||
SendConfirmWizardFragment.Listener,
|
||||
SendSuccessWizardFragment.Listener,
|
||||
OnBackPressedListener, OnUriScannedListener {
|
||||
|
||||
final static public int MIXIN = 0;
|
||||
|
||||
private Listener activityCallback;
|
||||
|
||||
public interface Listener {
|
||||
SharedPreferences getPrefs();
|
||||
|
||||
long getTotalFunds();
|
||||
|
||||
boolean isStreetMode();
|
||||
|
||||
void onPrepareSend(String tag, TxData data);
|
||||
|
||||
String getWalletName();
|
||||
|
||||
void onSend(UserNotes notes);
|
||||
|
||||
void onDisposeRequest();
|
||||
|
||||
void onFragmentDone();
|
||||
|
||||
void setToolbarButton(int type);
|
||||
|
||||
void setTitle(String title);
|
||||
|
||||
void setSubtitle(String subtitle);
|
||||
|
||||
void setOnUriScannedListener(OnUriScannedListener onUriScannedListener);
|
||||
}
|
||||
|
||||
private View llNavBar;
|
||||
private DotBar dotBar;
|
||||
private Button bPrev;
|
||||
private Button bNext;
|
||||
|
||||
private Button bDone;
|
||||
|
||||
static private final int MAX_FALLBACK = Integer.MAX_VALUE;
|
||||
|
||||
public static SendFragment newInstance(String uri) {
|
||||
SendFragment f = new SendFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(WalletActivity.REQUEST_URI, uri);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
final View view = inflater.inflate(R.layout.fragment_send, container, false);
|
||||
|
||||
llNavBar = view.findViewById(R.id.llNavBar);
|
||||
bDone = view.findViewById(R.id.bDone);
|
||||
|
||||
dotBar = view.findViewById(R.id.dotBar);
|
||||
bPrev = view.findViewById(R.id.bPrev);
|
||||
bNext = view.findViewById(R.id.bNext);
|
||||
|
||||
ViewGroup llNotice = view.findViewById(R.id.llNotice);
|
||||
Notice.showAll(llNotice, ".*_send");
|
||||
|
||||
spendViewPager = view.findViewById(R.id.pager);
|
||||
pagerAdapter = new SpendPagerAdapter(getChildFragmentManager());
|
||||
spendViewPager.setOffscreenPageLimit(pagerAdapter.getCount()); // load & keep all pages in cache
|
||||
spendViewPager.setAdapter(pagerAdapter);
|
||||
|
||||
spendViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
private int fallbackPosition = MAX_FALLBACK;
|
||||
private int currentPosition = 0;
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int newPosition) {
|
||||
Timber.d("onPageSelected=%d/%d", newPosition, fallbackPosition);
|
||||
if (fallbackPosition < newPosition) {
|
||||
spendViewPager.setCurrentItem(fallbackPosition);
|
||||
} else {
|
||||
pagerAdapter.getFragment(currentPosition).onPauseFragment();
|
||||
pagerAdapter.getFragment(newPosition).onResumeFragment();
|
||||
updatePosition(newPosition);
|
||||
currentPosition = newPosition;
|
||||
fallbackPosition = MAX_FALLBACK;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
if (state == ViewPager.SCROLL_STATE_DRAGGING) {
|
||||
if (!spendViewPager.validateFields(spendViewPager.getCurrentItem())) {
|
||||
fallbackPosition = spendViewPager.getCurrentItem();
|
||||
} else {
|
||||
fallbackPosition = spendViewPager.getCurrentItem() + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bPrev.setOnClickListener(v -> spendViewPager.previous());
|
||||
|
||||
bNext.setOnClickListener(v -> spendViewPager.next());
|
||||
|
||||
bDone.setOnClickListener(v -> {
|
||||
Timber.d("bDone.onClick");
|
||||
activityCallback.onFragmentDone();
|
||||
});
|
||||
|
||||
updatePosition(0);
|
||||
|
||||
final EditText etDummy = view.findViewById(R.id.etDummy);
|
||||
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
etDummy.requestFocus();
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
Bundle args = getArguments();
|
||||
if (args != null) {
|
||||
String uri = args.getString(WalletActivity.REQUEST_URI);
|
||||
Timber.d("URI: %s", uri);
|
||||
if (uri != null) {
|
||||
barcodeData = BarcodeData.fromString(uri);
|
||||
Timber.d("barcodeData: %s", barcodeData != null ? barcodeData.toString() : "null");
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
void updatePosition(int position) {
|
||||
dotBar.setActiveDot(position);
|
||||
CharSequence nextLabel = pagerAdapter.getPageTitle(position + 1);
|
||||
bNext.setText(nextLabel);
|
||||
if (nextLabel != null) {
|
||||
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_navigate_next, 0);
|
||||
} else {
|
||||
bNext.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
CharSequence prevLabel = pagerAdapter.getPageTitle(position - 1);
|
||||
bPrev.setText(prevLabel);
|
||||
if (prevLabel != null) {
|
||||
bPrev.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_navigate_prev, 0, 0, 0);
|
||||
} else {
|
||||
bPrev.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
Timber.d("onResume");
|
||||
activityCallback.setSubtitle(getString(R.string.send_title));
|
||||
if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_SUCCESS) {
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
|
||||
} else {
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_CANCEL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
Timber.d("onAttach %s", context);
|
||||
super.onAttach(context);
|
||||
if (context instanceof Listener) {
|
||||
activityCallback = (Listener) context;
|
||||
activityCallback.setOnUriScannedListener(this);
|
||||
} else {
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
activityCallback.setOnUriScannedListener(null);
|
||||
super.onDetach();
|
||||
}
|
||||
|
||||
private SpendViewPager spendViewPager;
|
||||
private SpendPagerAdapter pagerAdapter;
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (isComitted()) return true; // no going back
|
||||
if (spendViewPager.getCurrentItem() == 0) {
|
||||
return false;
|
||||
} else {
|
||||
spendViewPager.previous();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUriScanned(BarcodeData barcodeData) {
|
||||
if (spendViewPager.getCurrentItem() == SpendPagerAdapter.POS_ADDRESS) {
|
||||
final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_ADDRESS);
|
||||
if (fragment instanceof SendAddressWizardFragment) {
|
||||
((SendAddressWizardFragment) fragment).processScannedData(barcodeData);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
XMR, BTC
|
||||
}
|
||||
|
||||
Mode mode = Mode.XMR;
|
||||
|
||||
@Override
|
||||
public void setMode(Mode aMode) {
|
||||
if (mode != aMode) {
|
||||
mode = aMode;
|
||||
switch (aMode) {
|
||||
case XMR:
|
||||
txData = new TxData();
|
||||
break;
|
||||
case BTC:
|
||||
txData = new TxDataBtc();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
|
||||
}
|
||||
getView().post(() -> pagerAdapter.notifyDataSetChanged());
|
||||
Timber.d("New Mode = %s", mode.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public class SpendPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private static final int POS_ADDRESS = 0;
|
||||
private static final int POS_AMOUNT = 1;
|
||||
private static final int POS_CONFIRM = 2;
|
||||
private static final int POS_SUCCESS = 3;
|
||||
private int numPages = 3;
|
||||
|
||||
SparseArray<WeakReference<SendWizardFragment>> myFragments = new SparseArray<>();
|
||||
|
||||
public SpendPagerAdapter(FragmentManager fm) {
|
||||
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
}
|
||||
|
||||
public void addSuccess() {
|
||||
numPages++;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return numPages;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
Timber.d("instantiateItem %d", position);
|
||||
SendWizardFragment fragment = (SendWizardFragment) super.instantiateItem(container, position);
|
||||
myFragments.put(position, new WeakReference<>(fragment));
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
Timber.d("destroyItem %d", position);
|
||||
myFragments.remove(position);
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
public SendWizardFragment getFragment(int position) {
|
||||
WeakReference<SendWizardFragment> ref = myFragments.get(position);
|
||||
if (ref != null)
|
||||
return myFragments.get(position).get();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SendWizardFragment getItem(int position) {
|
||||
Timber.d("getItem(%d) CREATE", position);
|
||||
Timber.d("Mode=%s", mode.toString());
|
||||
if (mode == Mode.XMR) {
|
||||
switch (position) {
|
||||
case POS_ADDRESS:
|
||||
return SendAddressWizardFragment.newInstance(SendFragment.this);
|
||||
case POS_AMOUNT:
|
||||
return SendAmountWizardFragment.newInstance(SendFragment.this);
|
||||
case POS_CONFIRM:
|
||||
return SendConfirmWizardFragment.newInstance(SendFragment.this);
|
||||
case POS_SUCCESS:
|
||||
return SendSuccessWizardFragment.newInstance(SendFragment.this);
|
||||
default:
|
||||
throw new IllegalArgumentException("no such send position(" + position + ")");
|
||||
}
|
||||
} else if (mode == Mode.BTC) {
|
||||
switch (position) {
|
||||
case POS_ADDRESS:
|
||||
return SendAddressWizardFragment.newInstance(SendFragment.this);
|
||||
case POS_AMOUNT:
|
||||
return SendBtcAmountWizardFragment.newInstance(SendFragment.this);
|
||||
case POS_CONFIRM:
|
||||
return SendBtcConfirmWizardFragment.newInstance(SendFragment.this);
|
||||
case POS_SUCCESS:
|
||||
return SendBtcSuccessWizardFragment.newInstance(SendFragment.this);
|
||||
default:
|
||||
throw new IllegalArgumentException("no such send position(" + position + ")");
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown mode!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
Timber.d("getPageTitle(%d)", position);
|
||||
if (position >= numPages) return null;
|
||||
switch (position) {
|
||||
case POS_ADDRESS:
|
||||
return getString(R.string.send_address_title);
|
||||
case POS_AMOUNT:
|
||||
return getString(R.string.send_amount_title);
|
||||
case POS_CONFIRM:
|
||||
return getString(R.string.send_confirm_title);
|
||||
case POS_SUCCESS:
|
||||
return getString(R.string.send_success_title);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(@NonNull Object object) {
|
||||
Timber.d("getItemPosition %s", String.valueOf(object));
|
||||
if (object instanceof SendAddressWizardFragment) {
|
||||
// keep these pages
|
||||
return POSITION_UNCHANGED;
|
||||
} else {
|
||||
return POSITION_NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxData getTxData() {
|
||||
return txData;
|
||||
}
|
||||
|
||||
private TxData txData = new TxData();
|
||||
|
||||
private BarcodeData barcodeData;
|
||||
|
||||
// Listeners
|
||||
@Override
|
||||
public void setBarcodeData(BarcodeData data) {
|
||||
barcodeData = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarcodeData getBarcodeData() {
|
||||
return barcodeData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BarcodeData popBarcodeData() {
|
||||
Timber.d("POPPED");
|
||||
BarcodeData data = barcodeData;
|
||||
barcodeData = null;
|
||||
return data;
|
||||
}
|
||||
|
||||
boolean isComitted() {
|
||||
return committedTx != null;
|
||||
}
|
||||
|
||||
PendingTx committedTx;
|
||||
|
||||
@Override
|
||||
public PendingTx getCommittedTx() {
|
||||
return committedTx;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void commitTransaction() {
|
||||
Timber.d("REALLY SEND");
|
||||
disableNavigation(); // committed - disable all navigation
|
||||
activityCallback.onSend(txData.getUserNotes());
|
||||
committedTx = pendingTx;
|
||||
}
|
||||
|
||||
void disableNavigation() {
|
||||
spendViewPager.allowSwipe(false);
|
||||
}
|
||||
|
||||
void enableNavigation() {
|
||||
spendViewPager.allowSwipe(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableDone() {
|
||||
llNavBar.setVisibility(View.INVISIBLE);
|
||||
bDone.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public Listener getActivityCallback() {
|
||||
return activityCallback;
|
||||
}
|
||||
|
||||
|
||||
// callbacks from send service
|
||||
|
||||
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
|
||||
final SendConfirm confirm = getSendConfirm();
|
||||
if (confirm != null) {
|
||||
pendingTx = new PendingTx(pendingTransaction);
|
||||
confirm.transactionCreated(txTag, pendingTransaction);
|
||||
} else {
|
||||
// not in confirm fragment => dispose & move on
|
||||
disposeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disposeTransaction() {
|
||||
pendingTx = null;
|
||||
activityCallback.onDisposeRequest();
|
||||
}
|
||||
|
||||
PendingTx pendingTx;
|
||||
|
||||
public PendingTx getPendingTx() {
|
||||
return pendingTx;
|
||||
}
|
||||
|
||||
public void onCreateTransactionFailed(String errorText) {
|
||||
final SendConfirm confirm = getSendConfirm();
|
||||
if (confirm != null) {
|
||||
confirm.createTransactionFailed(errorText);
|
||||
}
|
||||
}
|
||||
|
||||
SendConfirm getSendConfirm() {
|
||||
final SendWizardFragment fragment = pagerAdapter.getFragment(SpendPagerAdapter.POS_CONFIRM);
|
||||
if (fragment instanceof SendConfirm) {
|
||||
return (SendConfirm) fragment;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void onTransactionSent(final String txId) {
|
||||
Timber.d("txid=%s", txId);
|
||||
pagerAdapter.addSuccess();
|
||||
Timber.d("numPages=%d", spendViewPager.getAdapter().getCount());
|
||||
activityCallback.setToolbarButton(Toolbar.BUTTON_NONE);
|
||||
spendViewPager.setCurrentItem(SpendPagerAdapter.POS_SUCCESS);
|
||||
}
|
||||
|
||||
public void onSendTransactionFailed(final String error) {
|
||||
Timber.d("error=%s", error);
|
||||
committedTx = null;
|
||||
final SendConfirm confirm = getSendConfirm();
|
||||
if (confirm != null) {
|
||||
confirm.sendFailed(getString(R.string.status_transaction_failed, error));
|
||||
}
|
||||
enableNavigation();
|
||||
}
|
||||
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
final MaterialContainerTransform transform = new MaterialContainerTransform();
|
||||
transform.setDrawingViewId(R.id.fragment_container);
|
||||
transform.setDuration(getResources().getInteger(R.integer.tx_item_transition_duration));
|
||||
transform.setAllContainerColors(ThemeHelper.getThemedColor(getContext(), android.R.attr.colorBackground));
|
||||
setSharedElementEnterTransition(transform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.send_menu, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.PendingTx;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SendSuccessWizardFragment extends SendWizardFragment {
|
||||
|
||||
public static SendSuccessWizardFragment newInstance(Listener listener) {
|
||||
SendSuccessWizardFragment instance = new SendSuccessWizardFragment();
|
||||
instance.setSendListener(listener);
|
||||
return instance;
|
||||
}
|
||||
|
||||
Listener sendListener;
|
||||
|
||||
public SendSuccessWizardFragment setSendListener(Listener listener) {
|
||||
this.sendListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
TxData getTxData();
|
||||
|
||||
PendingTx getCommittedTx();
|
||||
|
||||
void enableDone();
|
||||
|
||||
SendFragment.Mode getMode();
|
||||
|
||||
SendFragment.Listener getActivityCallback();
|
||||
}
|
||||
|
||||
ImageButton bCopyTxId;
|
||||
private TextView tvTxId;
|
||||
private TextView tvTxAddress;
|
||||
private TextView tvTxPaymentId;
|
||||
private TextView tvTxAmount;
|
||||
private TextView tvTxFee;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
|
||||
|
||||
View view = inflater.inflate(
|
||||
R.layout.fragment_send_success, container, false);
|
||||
|
||||
bCopyTxId = view.findViewById(R.id.bCopyTxId);
|
||||
bCopyTxId.setEnabled(false);
|
||||
bCopyTxId.setOnClickListener(v -> {
|
||||
Helper.clipBoardCopy(getActivity(), 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);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPauseFragment() {
|
||||
super.onPauseFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
final TxData txData = sendListener.getTxData();
|
||||
tvTxAddress.setText(txData.getDestinationAddress());
|
||||
|
||||
final PendingTx committedTx = sendListener.getCommittedTx();
|
||||
if (committedTx != null) {
|
||||
tvTxId.setText(committedTx.txId);
|
||||
bCopyTxId.setEnabled(true);
|
||||
|
||||
if (sendListener.getActivityCallback().isStreetMode()
|
||||
&& (sendListener.getTxData().getAmount() == Wallet.SWEEP_ALL)) {
|
||||
tvTxAmount.setText(getString(R.string.street_sweep_amount));
|
||||
} else {
|
||||
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
|
||||
}
|
||||
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
|
||||
}
|
||||
sendListener.enableDone();
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.fragment.send;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.m2049r.xmrwallet.layout.SpendViewPager;
|
||||
|
||||
abstract public class SendWizardFragment extends Fragment
|
||||
implements SpendViewPager.OnValidateFieldsListener {
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onPauseFragment() {
|
||||
}
|
||||
|
||||
public void onResumeFragment() {
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.m2049r.xmrwallet.fragment.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
|
||||
public class SettingsFragment extends Fragment {
|
||||
|
||||
private SettingsViewModel mViewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_settings, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mViewModel = new ViewModelProvider(this).get(SettingsViewModel.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.m2049r.xmrwallet.fragment.settings;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
public class SettingsViewModel extends ViewModel{
|
||||
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 yorha-0x
|
||||
*
|
||||
* 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.layout;
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class DiffCallback<T> extends DiffUtil.Callback {
|
||||
|
||||
protected final List<T> mOldList;
|
||||
protected final List<T> mNewList;
|
||||
|
||||
public DiffCallback(List<T> oldList, List<T> newList) {
|
||||
this.mOldList = oldList;
|
||||
this.mNewList = newList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
return mOldList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize() {
|
||||
return mNewList.size();
|
||||
}
|
||||
|
||||
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
|
||||
|
||||
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018 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.layout;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHolder> {
|
||||
public interface OnInteractionListener {
|
||||
void onInteraction(View view, NodeInfo item);
|
||||
|
||||
boolean onLongInteraction(View view, NodeInfo item);
|
||||
}
|
||||
|
||||
private final List<NodeInfo> nodeItems = new ArrayList<>();
|
||||
private final OnInteractionListener listener;
|
||||
|
||||
private final FragmentActivity activity;
|
||||
|
||||
public NodeInfoAdapter(FragmentActivity activity, OnInteractionListener listener) {
|
||||
this.activity = activity;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void notifyItemChanged(NodeInfo nodeInfo) {
|
||||
final int pos = nodeItems.indexOf(nodeInfo);
|
||||
if (pos >= 0) notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
private static class NodeDiff extends DiffCallback<NodeInfo> {
|
||||
|
||||
public NodeDiff(List<NodeInfo> oldList, List<NodeInfo> newList) {
|
||||
super(oldList, newList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
final NodeInfo oldItem = mOldList.get(oldItemPosition);
|
||||
final NodeInfo newItem = mNewList.get(newItemPosition);
|
||||
return (oldItem.getTimestamp() == newItem.getTimestamp())
|
||||
&& (oldItem.isTested() == newItem.isTested())
|
||||
&& (oldItem.isValid() == newItem.isValid())
|
||||
&& (oldItem.getResponseTime() == newItem.getResponseTime())
|
||||
&& (oldItem.isSelected() == newItem.isSelected())
|
||||
&& (oldItem.getName().equals(newItem.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_node, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final @NonNull ViewHolder holder, int position) {
|
||||
holder.bind(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return nodeItems.size();
|
||||
}
|
||||
|
||||
public void addNode(NodeInfo node) {
|
||||
List<NodeInfo> newItems = new ArrayList<>(nodeItems);
|
||||
if (!nodeItems.contains(node))
|
||||
newItems.add(node);
|
||||
setNodes(newItems); // in case the nodeinfo has changed
|
||||
}
|
||||
|
||||
public void setNodes(Collection<NodeInfo> newItemsCollection) {
|
||||
List<NodeInfo> newItems;
|
||||
if (newItemsCollection != null) {
|
||||
newItems = new ArrayList<>(newItemsCollection);
|
||||
Collections.sort(newItems, NodeInfo.BestNodeComparator);
|
||||
} else {
|
||||
newItems = new ArrayList<>();
|
||||
}
|
||||
final NodeInfoAdapter.NodeDiff diffCallback = new NodeInfoAdapter.NodeDiff(nodeItems, newItems);
|
||||
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
|
||||
nodeItems.clear();
|
||||
nodeItems.addAll(newItems);
|
||||
diffResult.dispatchUpdatesTo(this);
|
||||
}
|
||||
|
||||
public void setNodes() {
|
||||
setNodes(nodeItems);
|
||||
}
|
||||
|
||||
private boolean itemsClickable = true;
|
||||
|
||||
public void allowClick(boolean clickable) {
|
||||
itemsClickable = clickable;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
final ImageButton ibBookmark;
|
||||
final View pbBookmark;
|
||||
final TextView tvName;
|
||||
final TextView tvInfo;
|
||||
final ImageView ivPing;
|
||||
NodeInfo nodeItem;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ibBookmark = itemView.findViewById(R.id.ibBookmark);
|
||||
pbBookmark = itemView.findViewById(R.id.pbBookmark);
|
||||
tvName = itemView.findViewById(R.id.tvName);
|
||||
tvInfo = itemView.findViewById(R.id.tvInfo);
|
||||
ivPing = itemView.findViewById(R.id.ivPing);
|
||||
ibBookmark.setOnClickListener(v -> {
|
||||
nodeItem.toggleFavourite();
|
||||
showStar();
|
||||
if (!nodeItem.isFavourite()) {
|
||||
nodeItem.setSelected(false);
|
||||
setNodes(nodeItems);
|
||||
}
|
||||
});
|
||||
itemView.setOnClickListener(this);
|
||||
itemView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
private void showStar() {
|
||||
if (nodeItem.isFavourite()) {
|
||||
ibBookmark.setImageResource(R.drawable.ic_favorite_24dp);
|
||||
} else {
|
||||
ibBookmark.setImageResource(R.drawable.ic_favorite_border_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
void bind(int position) {
|
||||
nodeItem = nodeItems.get(position);
|
||||
tvName.setText(nodeItem.getName());
|
||||
ivPing.setImageResource(getPingIcon(nodeItem));
|
||||
if (nodeItem.isTested()) {
|
||||
if (nodeItem.isValid()) {
|
||||
nodeItem.showInfo(tvInfo);
|
||||
} else {
|
||||
nodeItem.showInfo(tvInfo, getResponseErrorText(activity, nodeItem.getResponseCode()), true);
|
||||
}
|
||||
} else {
|
||||
nodeItem.showInfo(tvInfo);
|
||||
}
|
||||
itemView.setSelected(nodeItem.isSelected());
|
||||
itemView.setClickable(itemsClickable);
|
||||
itemView.setEnabled(itemsClickable);
|
||||
ibBookmark.setClickable(itemsClickable);
|
||||
pbBookmark.setVisibility(nodeItem.isSelecting() ? View.VISIBLE : View.INVISIBLE);
|
||||
showStar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
int position = getAdapterPosition(); // 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()) {
|
||||
switch (NetCipherHelper.getStatus()) {
|
||||
case NOT_INSTALLED:
|
||||
HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor);
|
||||
return;
|
||||
case DISABLED:
|
||||
HelpFragment.display(activity.getSupportFragmentManager(), R.string.help_tor_enable);
|
||||
return;
|
||||
}
|
||||
}
|
||||
node.setSelecting(true);
|
||||
allowClick(false);
|
||||
listener.onInteraction(view, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (listener != null) {
|
||||
int position = getAdapterPosition(); // 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));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static public int getPingIcon(NodeInfo nodeInfo) {
|
||||
if (nodeInfo.isUnauthorized()) {
|
||||
return R.drawable.ic_wifi_lock;
|
||||
}
|
||||
if (nodeInfo.isValid()) {
|
||||
final double ping = nodeInfo.getResponseTime();
|
||||
if (ping < NodeInfo.PING_GOOD) {
|
||||
return R.drawable.ic_wifi_4_bar;
|
||||
} else if (ping < NodeInfo.PING_MEDIUM) {
|
||||
return R.drawable.ic_wifi_3_bar;
|
||||
} else if (ping < NodeInfo.PING_BAD) {
|
||||
return R.drawable.ic_wifi_2_bar;
|
||||
} else {
|
||||
return R.drawable.ic_wifi_1_bar;
|
||||
}
|
||||
} else {
|
||||
return R.drawable.ic_wifi_off;
|
||||
}
|
||||
}
|
||||
|
||||
static public String getResponseErrorText(Context ctx, int responseCode) {
|
||||
if (responseCode == 0) {
|
||||
return ctx.getResources().getString(R.string.node_general_error);
|
||||
} else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
return ctx.getResources().getString(R.string.node_auth_error);
|
||||
} else if (responseCode == 418) {
|
||||
return ctx.getResources().getString(R.string.node_tor_error);
|
||||
} else {
|
||||
return ctx.getResources().getString(R.string.node_test_error, responseCode);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.layout;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.m2049r.xmrwallet.fragment.send.SendFragment;
|
||||
|
||||
public class SpendViewPager extends ViewPager {
|
||||
|
||||
public interface OnValidateFieldsListener {
|
||||
boolean onValidateFields();
|
||||
}
|
||||
|
||||
public SpendViewPager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public SpendViewPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void next() {
|
||||
int pos = getCurrentItem();
|
||||
if (validateFields(pos)) {
|
||||
setCurrentItem(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void previous() {
|
||||
setCurrentItem(getCurrentItem() - 1);
|
||||
}
|
||||
|
||||
private boolean allowSwipe = true;
|
||||
|
||||
public void allowSwipe(boolean allow) {
|
||||
allowSwipe = allow;
|
||||
}
|
||||
|
||||
public boolean validateFields(int position) {
|
||||
OnValidateFieldsListener c = ((SendFragment.SpendPagerAdapter) getAdapter()).getFragment(position);
|
||||
return c.onValidateFields();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
if (allowSwipe) return super.onInterceptTouchEvent(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (allowSwipe) return super.onTouchEvent(event);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.layout;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.Subaddress;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAdapter.ViewHolder> {
|
||||
public interface OnInteractionListener {
|
||||
void onInteraction(View view, Subaddress item);
|
||||
|
||||
boolean onLongInteraction(View view, Subaddress item);
|
||||
}
|
||||
|
||||
private final List<Subaddress> items;
|
||||
private final OnInteractionListener listener;
|
||||
|
||||
Context context;
|
||||
|
||||
public SubaddressInfoAdapter(Context context, OnInteractionListener listener) {
|
||||
this.context = context;
|
||||
this.items = new ArrayList<>();
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private static class SubaddressInfoDiff extends DiffCallback<Subaddress> {
|
||||
|
||||
public SubaddressInfoDiff(List<Subaddress> oldList, List<Subaddress> newList) {
|
||||
super(oldList, newList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return mOldList.get(oldItemPosition).getAddress().equals(mNewList.get(newItemPosition).getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return mOldList.get(oldItemPosition).equals(mNewList.get(newItemPosition));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_subaddress, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
||||
holder.bind(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
public Subaddress getItem(int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
public void setInfos(List<Subaddress> newItems) {
|
||||
if (newItems == null) {
|
||||
newItems = new ArrayList<>();
|
||||
Timber.d("setInfos null");
|
||||
} else {
|
||||
Timber.d("setInfos %s", newItems.size());
|
||||
}
|
||||
Collections.sort(newItems);
|
||||
final DiffCallback<Subaddress> diffCallback = new SubaddressInfoDiff(items, newItems);
|
||||
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
|
||||
items.clear();
|
||||
items.addAll(newItems);
|
||||
diffResult.dispatchUpdatesTo(this);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
final TextView tvName;
|
||||
final TextView tvAddress;
|
||||
final TextView tvAmount;
|
||||
Subaddress item;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvName = itemView.findViewById(R.id.tvName);
|
||||
tvAddress = itemView.findViewById(R.id.tvAddress);
|
||||
tvAmount = itemView.findViewById(R.id.tx_amount);
|
||||
itemView.setOnClickListener(this);
|
||||
itemView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
void bind(int position) {
|
||||
item = getItem(position);
|
||||
itemView.setTransitionName(context.getString(R.string.subaddress_item_transition_name, item.getAddressIndex()));
|
||||
|
||||
final String label = item.getDisplayLabel();
|
||||
final String address = context.getString(R.string.subbaddress_info_subtitle,
|
||||
item.getAddressIndex(), item.getSquashedAddress());
|
||||
tvName.setText(label.isEmpty() ? address : label);
|
||||
tvAddress.setText(address);
|
||||
final long amount = item.getAmount();
|
||||
if (amount > 0)
|
||||
tvAmount.setText(context.getString(R.string.tx_list_amount_positive,
|
||||
Helper.getDisplayAmount(amount, Helper.DISPLAY_DIGITS_INFO)));
|
||||
else
|
||||
tvAmount.setText("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
int position = getAdapterPosition(); // 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (listener != null) {
|
||||
int position = getAdapterPosition(); // 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));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.layout;
|
||||
|
||||
import android.content.Context;
|
||||
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 androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.data.UserNotes;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
|
||||
private final static SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
private final int outboundColour;
|
||||
private final int inboundColour;
|
||||
private final int pendingColour;
|
||||
private final int failedColour;
|
||||
|
||||
public interface OnInteractionListener {
|
||||
void onInteraction(View view, TransactionInfo item);
|
||||
}
|
||||
|
||||
private final List<TransactionInfo> infoItems;
|
||||
private final OnInteractionListener listener;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public TransactionInfoAdapter(Context context, OnInteractionListener listener) {
|
||||
this.context = context;
|
||||
inboundColour = ThemeHelper.getThemedColor(context, R.attr.positiveColor);
|
||||
outboundColour = ThemeHelper.getThemedColor(context, R.attr.negativeColor);
|
||||
pendingColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor);
|
||||
failedColour = ThemeHelper.getThemedColor(context, R.attr.neutralColor);
|
||||
infoItems = new ArrayList<>();
|
||||
this.listener = listener;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
TimeZone tz = cal.getTimeZone(); //get the local time zone.
|
||||
DATETIME_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
public boolean needsTransactionUpdateOnNewBlock() {
|
||||
return (infoItems.size() > 0) && !infoItems.get(0).isConfirmed();
|
||||
}
|
||||
|
||||
private static class TransactionInfoDiff extends DiffCallback<TransactionInfo> {
|
||||
|
||||
public TransactionInfoDiff(List<TransactionInfo> oldList, List<TransactionInfo> newList) {
|
||||
super(oldList, newList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return mOldList.get(oldItemPosition).hash.equals(mNewList.get(newItemPosition).hash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
final TransactionInfo oldItem = mOldList.get(oldItemPosition);
|
||||
final TransactionInfo newItem = mNewList.get(newItemPosition);
|
||||
return (oldItem.direction == newItem.direction)
|
||||
&& (oldItem.isPending == newItem.isPending)
|
||||
&& (oldItem.isFailed == newItem.isFailed)
|
||||
&& ((oldItem.confirmations == newItem.confirmations) || (oldItem.isConfirmed()))
|
||||
&& (oldItem.subaddressLabel.equals(newItem.subaddressLabel))
|
||||
&& (Objects.equals(oldItem.notes, newItem.notes));
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
||||
holder.bind(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return infoItems.size();
|
||||
}
|
||||
|
||||
public void setInfos(List<TransactionInfo> newItems) {
|
||||
if (newItems == null) {
|
||||
newItems = new ArrayList<>();
|
||||
Timber.d("setInfos null");
|
||||
} else {
|
||||
Timber.d("setInfos %s", newItems.size());
|
||||
}
|
||||
Collections.sort(newItems);
|
||||
final DiffCallback<TransactionInfo> diffCallback = new TransactionInfoAdapter.TransactionInfoDiff(infoItems, newItems);
|
||||
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
|
||||
infoItems.clear();
|
||||
infoItems.addAll(newItems);
|
||||
diffResult.dispatchUpdatesTo(this);
|
||||
}
|
||||
|
||||
public void removeItem(int position) {
|
||||
List<TransactionInfo> newItems = new ArrayList<>(infoItems);
|
||||
if (newItems.size() > position)
|
||||
newItems.remove(position);
|
||||
setInfos(newItems); // in case the nodeinfo has changed
|
||||
}
|
||||
|
||||
public TransactionInfo getItem(int position) {
|
||||
return infoItems.get(position);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final ImageView ivTxType;
|
||||
final TextView tvAmount;
|
||||
final TextView tvFailed;
|
||||
final TextView tvPaymentId;
|
||||
final TextView tvDateTime;
|
||||
final CircularProgressIndicator pbConfirmations;
|
||||
final TextView tvConfirmations;
|
||||
TransactionInfo infoItem;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ivTxType = itemView.findViewById(R.id.ivTxType);
|
||||
tvAmount = itemView.findViewById(R.id.tx_amount);
|
||||
tvFailed = itemView.findViewById(R.id.tx_failed);
|
||||
tvPaymentId = itemView.findViewById(R.id.tx_paymentid);
|
||||
tvDateTime = itemView.findViewById(R.id.tx_datetime);
|
||||
pbConfirmations = itemView.findViewById(R.id.pbConfirmations);
|
||||
pbConfirmations.setMax(TransactionInfo.CONFIRMATION);
|
||||
tvConfirmations = itemView.findViewById(R.id.tvConfirmations);
|
||||
}
|
||||
|
||||
private String getDateTime(long time) {
|
||||
return DATETIME_FORMATTER.format(new Date(time * 1000));
|
||||
}
|
||||
|
||||
private void setTxColour(int clr) {
|
||||
tvAmount.setTextColor(clr);
|
||||
}
|
||||
|
||||
void bind(int position) {
|
||||
infoItem = infoItems.get(position);
|
||||
itemView.setTransitionName(context.getString(R.string.tx_item_transition_name, infoItem.hash));
|
||||
|
||||
UserNotes userNotes = new UserNotes(infoItem.notes);
|
||||
if (userNotes.xmrtoKey != null) {
|
||||
final Crypto crypto = Crypto.withSymbol(userNotes.xmrtoCurrency);
|
||||
if (crypto != null) {
|
||||
ivTxType.setImageResource(crypto.getIconEnabledId());
|
||||
ivTxType.setVisibility(View.VISIBLE);
|
||||
} else {// otherwirse pretend we don't know it's a shift
|
||||
ivTxType.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
ivTxType.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String displayAmount = Helper.getDisplayAmount(infoItem.amount, Helper.DISPLAY_DIGITS_INFO);
|
||||
if (infoItem.direction == TransactionInfo.Direction.Direction_Out) {
|
||||
tvAmount.setText(context.getString(R.string.tx_list_amount_negative, displayAmount));
|
||||
} else {
|
||||
tvAmount.setText(context.getString(R.string.tx_list_amount_positive, displayAmount));
|
||||
}
|
||||
|
||||
tvFailed.setVisibility(View.GONE);
|
||||
if (infoItem.isFailed) {
|
||||
this.tvAmount.setText(context.getString(R.string.tx_list_amount_failed, displayAmount));
|
||||
tvFailed.setVisibility(View.VISIBLE);
|
||||
setTxColour(failedColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else if (infoItem.isPending) {
|
||||
setTxColour(pendingColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
pbConfirmations.setIndeterminate(true);
|
||||
pbConfirmations.setVisibility(View.VISIBLE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else if (infoItem.direction == TransactionInfo.Direction.Direction_In) {
|
||||
setTxColour(inboundColour);
|
||||
if (!infoItem.isConfirmed()) {
|
||||
pbConfirmations.setVisibility(View.VISIBLE);
|
||||
final int confirmations = (int) infoItem.confirmations;
|
||||
pbConfirmations.setProgressCompat(confirmations, true);
|
||||
final String confCount = Integer.toString(confirmations);
|
||||
tvConfirmations.setText(confCount);
|
||||
if (confCount.length() == 1) // we only have space for character in the progress circle
|
||||
tvConfirmations.setVisibility(View.VISIBLE);
|
||||
else
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
} else {
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
setTxColour(outboundColour);
|
||||
pbConfirmations.setVisibility(View.GONE);
|
||||
tvConfirmations.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String tag = null;
|
||||
String info = "";
|
||||
if ((infoItem.addressIndex != 0) && (infoItem.direction == TransactionInfo.Direction.Direction_In))
|
||||
tag = infoItem.getDisplayLabel();
|
||||
if ((userNotes.note.isEmpty())) {
|
||||
if (!infoItem.paymentId.equals("0000000000000000")) {
|
||||
info = infoItem.paymentId;
|
||||
}
|
||||
} else {
|
||||
info = userNotes.note;
|
||||
}
|
||||
if (tag == null) {
|
||||
tvPaymentId.setText(info);
|
||||
} else {
|
||||
Spanned label = Html.fromHtml(context.getString(R.string.tx_details_notes,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
tag, info.isEmpty() ? "" : (" " + info)));
|
||||
tvPaymentId.setText(label);
|
||||
}
|
||||
|
||||
this.tvDateTime.setText(getDateTime(infoItem.timestamp));
|
||||
|
||||
itemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
int position = getAdapterPosition(); // 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.layout;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.ViewHolder> {
|
||||
|
||||
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
public interface OnInteractionListener {
|
||||
void onInteraction(View view, WalletManager.WalletInfo item);
|
||||
|
||||
boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo infoItem);
|
||||
}
|
||||
|
||||
private final List<WalletManager.WalletInfo> infoItems;
|
||||
private final OnInteractionListener listener;
|
||||
|
||||
Context context;
|
||||
|
||||
public WalletInfoAdapter(Context context, OnInteractionListener listener) {
|
||||
this.context = context;
|
||||
this.infoItems = new ArrayList<>();
|
||||
this.listener = listener;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
TimeZone tz = cal.getTimeZone(); //get the local time zone.
|
||||
DATETIME_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
private static class WalletInfoDiff extends DiffCallback<WalletManager.WalletInfo> {
|
||||
|
||||
public WalletInfoDiff(List<WalletManager.WalletInfo> oldList, List<WalletManager.WalletInfo> newList) {
|
||||
super(oldList, newList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return mOldList.get(oldItemPosition).getName().equals(mNewList.get(newItemPosition).getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return mOldList.get(oldItemPosition).compareTo(mNewList.get(newItemPosition)) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(
|
||||
LayoutInflater.from(parent.getContext()).inflate(R.layout.item_wallet, parent, false)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
||||
holder.bind(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return infoItems.size();
|
||||
}
|
||||
|
||||
public WalletManager.WalletInfo getItem(int position) {
|
||||
return infoItems.get(position);
|
||||
}
|
||||
|
||||
public void setInfos(List<WalletManager.WalletInfo> newItems) {
|
||||
if (newItems == null) {
|
||||
newItems = new ArrayList<>();
|
||||
Timber.d("setInfos null");
|
||||
} else {
|
||||
Timber.d("setInfos %s", newItems.size());
|
||||
}
|
||||
Collections.sort(newItems);
|
||||
final DiffCallback<WalletManager.WalletInfo> diffCallback = new WalletInfoAdapter.WalletInfoDiff(infoItems, newItems);
|
||||
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
|
||||
infoItems.clear();
|
||||
infoItems.addAll(newItems);
|
||||
diffResult.dispatchUpdatesTo(this);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final TextView tvName;
|
||||
final ImageButton ibOptions;
|
||||
WalletManager.WalletInfo infoItem;
|
||||
boolean popupOpen = false;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvName = itemView.findViewById(R.id.tvName);
|
||||
ibOptions = itemView.findViewById(R.id.ibOptions);
|
||||
ibOptions.setOnClickListener(view -> {
|
||||
if (popupOpen) return;
|
||||
//creating a popup menu
|
||||
PopupMenu popup = new PopupMenu(context, ibOptions);
|
||||
//inflating menu from xml resource
|
||||
popup.inflate(R.menu.list_context_menu);
|
||||
popupOpen = true;
|
||||
//adding click listener
|
||||
popup.setOnMenuItemClickListener(item -> {
|
||||
if (listener != null) {
|
||||
return listener.onContextInteraction(item, infoItem);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
//displaying the popup
|
||||
popup.show();
|
||||
popup.setOnDismissListener(menu -> popupOpen = false);
|
||||
|
||||
});
|
||||
itemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private String getDateTime(long time) {
|
||||
return DATETIME_FORMATTER.format(new Date(time * 1000));
|
||||
}
|
||||
|
||||
void bind(int position) {
|
||||
infoItem = infoItems.get(position);
|
||||
tvName.setText(infoItem.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (listener != null) {
|
||||
int position = getAdapterPosition(); // 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018 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.ledger;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.dialog.ProgressDialog;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LedgerProgressDialog extends ProgressDialog implements Ledger.Listener {
|
||||
|
||||
static public final int TYPE_DEBUG = 0;
|
||||
static public final int TYPE_RESTORE = 1;
|
||||
static public final int TYPE_SUBADDRESS = 2;
|
||||
static public final int TYPE_ACCOUNT = 3;
|
||||
static public final int TYPE_SEND = 4;
|
||||
|
||||
private final int type;
|
||||
private Handler uiHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
public LedgerProgressDialog(Context context, int type) {
|
||||
super(context);
|
||||
this.type = type;
|
||||
setCancelable(false);
|
||||
if (type == TYPE_SEND)
|
||||
setMessage(context.getString(R.string.info_prepare_tx));
|
||||
else
|
||||
setMessage(context.getString(R.string.progress_ledger_progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// prevent back button
|
||||
}
|
||||
|
||||
private int firstSubaddress = Integer.MAX_VALUE;
|
||||
|
||||
private boolean validate = false;
|
||||
private boolean validated = false;
|
||||
|
||||
@Override
|
||||
public void onInstructionSend(final Instruction ins, final byte[] apdu) {
|
||||
Timber.d("LedgerProgressDialog SEND %s", ins);
|
||||
uiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (type > TYPE_DEBUG) {
|
||||
validate = false;
|
||||
switch (ins) {
|
||||
case INS_RESET: // ledger may ask for confirmation - maybe a bug?
|
||||
case INS_GET_KEY: // ledger asks for confirmation to send keys
|
||||
case INS_DISPLAY_ADDRESS:
|
||||
setIndeterminate(true);
|
||||
setMessage(getContext().getString(R.string.progress_ledger_confirm));
|
||||
break;
|
||||
case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead
|
||||
//00 4a 00 00 09 00 01000000 30000000
|
||||
// 0 1 2 3 4 5 6 7 8 9 a b c d
|
||||
int account = bytesToInteger(apdu, 6);
|
||||
int subaddress = bytesToInteger(apdu, 10);
|
||||
Timber.d("fetching subaddress (%d, %d)", account, subaddress);
|
||||
switch (type) {
|
||||
case TYPE_RESTORE:
|
||||
setProgress(account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress + 1,
|
||||
Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES);
|
||||
setIndeterminate(false);
|
||||
break;
|
||||
case TYPE_ACCOUNT:
|
||||
final int requestedSubaddress = account * Ledger.LOOKAHEAD_SUBADDRESSES + subaddress;
|
||||
if (firstSubaddress > requestedSubaddress) {
|
||||
firstSubaddress = requestedSubaddress;
|
||||
}
|
||||
setProgress(requestedSubaddress - firstSubaddress + 1,
|
||||
Ledger.LOOKAHEAD_ACCOUNTS * Ledger.LOOKAHEAD_SUBADDRESSES);
|
||||
setIndeterminate(false);
|
||||
break;
|
||||
case TYPE_SUBADDRESS:
|
||||
if (firstSubaddress > subaddress) {
|
||||
firstSubaddress = subaddress;
|
||||
}
|
||||
setProgress(subaddress - firstSubaddress + 1, Ledger.LOOKAHEAD_SUBADDRESSES);
|
||||
setIndeterminate(false);
|
||||
break;
|
||||
default:
|
||||
setIndeterminate(true);
|
||||
break;
|
||||
}
|
||||
setMessage(getContext().getString(R.string.progress_ledger_lookahead));
|
||||
break;
|
||||
case INS_VERIFY_KEY:
|
||||
setIndeterminate(true);
|
||||
setMessage(getContext().getString(R.string.progress_ledger_verify));
|
||||
break;
|
||||
case INS_OPEN_TX:
|
||||
setIndeterminate(true);
|
||||
setMessage(getContext().getString(R.string.progress_ledger_opentx));
|
||||
break;
|
||||
case INS_MLSAG:
|
||||
if (validated) {
|
||||
setIndeterminate(true);
|
||||
setMessage(getContext().getString(R.string.progress_ledger_mlsag));
|
||||
}
|
||||
break;
|
||||
case INS_PREFIX_HASH:
|
||||
if ((apdu[2] != 1) || (apdu[3] != 0)) break;
|
||||
setIndeterminate(true);
|
||||
setMessage(getContext().getString(R.string.progress_ledger_confirm));
|
||||
break;
|
||||
case INS_VALIDATE:
|
||||
if ((apdu[2] != 1) || (apdu[3] != 1)) break;
|
||||
validate = true;
|
||||
uiHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (validate) {
|
||||
setIndeterminate(true);
|
||||
setMessage(getContext().getString(R.string.progress_ledger_confirm));
|
||||
validated = true;
|
||||
}
|
||||
}
|
||||
}, 250);
|
||||
break;
|
||||
default:
|
||||
// ignore others and maintain state
|
||||
}
|
||||
} else {
|
||||
setMessage(ins.name());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstructionReceive(final Instruction ins, final byte[] data) {
|
||||
Timber.d("LedgerProgressDialog RECV %s", ins);
|
||||
uiHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (type > TYPE_DEBUG) {
|
||||
switch (ins) {
|
||||
case INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY: // lookahead
|
||||
case INS_VERIFY_KEY:
|
||||
case INS_GET_CHACHA8_PREKEY:
|
||||
break;
|
||||
default:
|
||||
if (type != TYPE_SEND)
|
||||
setMessage(getContext().getString(R.string.progress_ledger_progress));
|
||||
}
|
||||
} else {
|
||||
setMessage("Returned from " + ins.name());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: we use ints in Java but the are signed; accounts & subaddresses are unsigned ...
|
||||
private int bytesToInteger(byte[] bytes, int offset) {
|
||||
int result = 0;
|
||||
for (int i = 3; i >= 0; i--) {
|
||||
result <<= 8;
|
||||
result |= (bytes[offset + i] & 0xFF);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2017 Google Inc.
|
||||
*
|
||||
* 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.livedata;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
|
||||
* navigation and Snackbar messages.
|
||||
* <p>
|
||||
* This avoids a common problem with events: on configuration change (like rotation) an update
|
||||
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
|
||||
* explicit call to setValue() or call().
|
||||
* <p>
|
||||
* Note that only one observer is going to be notified of changes.
|
||||
*/
|
||||
public class SingleLiveEvent<T> extends MutableLiveData<T> {
|
||||
|
||||
private static final String TAG = "SingleLiveEvent";
|
||||
|
||||
private final AtomicBoolean mPending = new AtomicBoolean(false);
|
||||
|
||||
@MainThread
|
||||
@Override
|
||||
public void observe(LifecycleOwner owner, final Observer<? super T> observer) {
|
||||
|
||||
if (hasActiveObservers()) {
|
||||
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
|
||||
}
|
||||
|
||||
// Observe the internal MutableLiveData
|
||||
super.observe(owner, new Observer<T>() {
|
||||
@Override
|
||||
public void onChanged(@Nullable T t) {
|
||||
if (mPending.compareAndSet(true, false)) {
|
||||
observer.onChanged(t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setValue(@Nullable T t) {
|
||||
mPending.set(true);
|
||||
super.setValue(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for cases where T is Void, to make calls cleaner.
|
||||
*/
|
||||
@MainThread
|
||||
public void call() {
|
||||
setValue(null);
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
|
||||
package com.m2049r.xmrwallet.model;
|
||||
|
||||
import com.m2049r.xmrwallet.XmrWalletApplication;
|
||||
import com.m2049r.xmrwallet.data.Node;
|
||||
import com.m2049r.xmrwallet.ledger.Ledger;
|
||||
import com.m2049r.xmrwallet.util.RestoreHeight;
|
||||
@ -25,7 +24,6 @@ import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
@ -248,7 +246,7 @@ public class WalletManager {
|
||||
//TODO virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0;
|
||||
|
||||
private String daemonAddress = null;
|
||||
private final NetworkType networkType = XmrWalletApplication.getNetworkType();
|
||||
private final NetworkType networkType = NetworkType.NetworkType_Mainnet;
|
||||
|
||||
public NetworkType getNetworkType() {
|
||||
return networkType;
|
||||
|
@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2020 EarlOfEgo, 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.onboarding;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.m2049r.xmrwallet.LoginActivity;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
|
||||
public class OnBoardingActivity extends AppCompatActivity implements OnBoardingAdapter.Listener {
|
||||
|
||||
private OnBoardingViewPager pager;
|
||||
private OnBoardingAdapter pagerAdapter;
|
||||
private Button nextButton;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_on_boarding);
|
||||
|
||||
nextButton = findViewById(R.id.buttonNext);
|
||||
|
||||
pager = findViewById(R.id.pager);
|
||||
pagerAdapter = new OnBoardingAdapter(this, this);
|
||||
pager.setAdapter(pagerAdapter);
|
||||
int pixels = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics());
|
||||
pager.setPageMargin(pixels);
|
||||
pager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
setButtonState(position);
|
||||
}
|
||||
});
|
||||
|
||||
final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
|
||||
if (pagerAdapter.getCount() > 1) {
|
||||
tabLayout.setupWithViewPager(pager, true);
|
||||
LinearLayout tabStrip = ((LinearLayout) tabLayout.getChildAt(0));
|
||||
for (int i = 0; i < tabStrip.getChildCount(); i++) {
|
||||
tabStrip.getChildAt(i).setClickable(false);
|
||||
}
|
||||
} else {
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
nextButton.setOnClickListener(v -> {
|
||||
final int item = pager.getCurrentItem();
|
||||
if (item + 1 >= pagerAdapter.getCount()) {
|
||||
finishOnboarding();
|
||||
} else {
|
||||
pager.setCurrentItem(item + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// let old users who have fingerprint wallets already agree for fingerprint sending
|
||||
OnBoardingScreen.FPSEND.setMustAgree(KeyStoreHelper.hasStoredPasswords(this));
|
||||
|
||||
for (int i = 0; i < OnBoardingScreen.values().length; i++) {
|
||||
agreed[i] = !OnBoardingScreen.values()[i].isMustAgree();
|
||||
}
|
||||
|
||||
setButtonState(0);
|
||||
}
|
||||
|
||||
private void finishOnboarding() {
|
||||
nextButton.setEnabled(false);
|
||||
OnBoardingManager.setOnBoardingShown(getApplicationContext());
|
||||
startActivity(new Intent(this, LoginActivity.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
boolean[] agreed = new boolean[OnBoardingScreen.values().length];
|
||||
|
||||
@Override
|
||||
public void setAgreeClicked(int position, boolean isChecked) {
|
||||
agreed[position] = isChecked;
|
||||
setButtonState(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAgreeClicked(int position) {
|
||||
return agreed[position];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setButtonState(int position) {
|
||||
nextButton.setEnabled(agreed[position]);
|
||||
if (nextButton.isEnabled())
|
||||
pager.setAllowedSwipeDirection(OnBoardingViewPager.SwipeDirection.ALL);
|
||||
else
|
||||
pager.setAllowedSwipeDirection(OnBoardingViewPager.SwipeDirection.LEFT);
|
||||
if (pager.getCurrentItem() + 1 == pagerAdapter.getCount()) { // last page
|
||||
nextButton.setText(R.string.onboarding_button_ready);
|
||||
} else {
|
||||
nextButton.setText(R.string.onboarding_button_next);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2020 EarlOfEgo, 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.onboarding;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class OnBoardingAdapter extends PagerAdapter {
|
||||
|
||||
interface Listener {
|
||||
void setAgreeClicked(int position, boolean isChecked);
|
||||
|
||||
boolean isAgreeClicked(int position);
|
||||
|
||||
void setButtonState(int position);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private Listener listener;
|
||||
|
||||
OnBoardingAdapter(final Context context, final Listener listener) {
|
||||
this.context = context;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
final View view = inflater.inflate(R.layout.view_onboarding, collection, false);
|
||||
final OnBoardingScreen onBoardingScreen = OnBoardingScreen.values()[position];
|
||||
|
||||
final Drawable drawable = ContextCompat.getDrawable(context, onBoardingScreen.getDrawable());
|
||||
((ImageView) view.findViewById(R.id.onboardingImage)).setImageDrawable(drawable);
|
||||
((TextView) view.findViewById(R.id.onboardingTitle)).setText(onBoardingScreen.getTitle());
|
||||
((TextView) view.findViewById(R.id.onboardingInformation)).setText(onBoardingScreen.getInformation());
|
||||
if (onBoardingScreen.isMustAgree()) {
|
||||
final CheckBox agree = ((CheckBox) view.findViewById(R.id.onboardingAgree));
|
||||
agree.setVisibility(View.VISIBLE);
|
||||
agree.setChecked(listener.isAgreeClicked(position));
|
||||
agree.setOnClickListener(v -> {
|
||||
listener.setAgreeClicked(position, ((CheckBox) v).isChecked());
|
||||
});
|
||||
}
|
||||
collection.addView(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return OnBoardingScreen.values().length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||
Timber.d("destroy " + position);
|
||||
collection.removeView((View) view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
|
||||
return view == object;
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2020 EarlOfEgo, 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.onboarding;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class OnBoardingManager {
|
||||
|
||||
private static final String PREFS_ONBOARDING = "PREFS_ONBOARDING";
|
||||
private static final String ONBOARDING_SHOWN = "ONBOARDING_SHOWN";
|
||||
|
||||
public static boolean shouldShowOnBoarding(final Context context) {
|
||||
return !getSharedPreferences(context).contains(ONBOARDING_SHOWN);
|
||||
}
|
||||
|
||||
public static void setOnBoardingShown(final Context context) {
|
||||
Timber.d("Set onboarding shown.");
|
||||
SharedPreferences sharedPreferences = getSharedPreferences(context);
|
||||
sharedPreferences.edit().putLong(ONBOARDING_SHOWN, new Date().getTime()).apply();
|
||||
}
|
||||
|
||||
public static void clearOnBoardingShown(final Context context) {
|
||||
SharedPreferences sharedPreferences = getSharedPreferences(context);
|
||||
sharedPreferences.edit().remove(ONBOARDING_SHOWN).apply();
|
||||
}
|
||||
|
||||
private static SharedPreferences getSharedPreferences(final Context context) {
|
||||
return context.getSharedPreferences(PREFS_ONBOARDING, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2020 EarlOfEgo, 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.onboarding;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
enum OnBoardingScreen {
|
||||
WELCOME(R.string.onboarding_welcome_title, R.string.onboarding_welcome_information, R.drawable.ic_onboarding_welcome, false),
|
||||
SEED(R.string.onboarding_seed_title, R.string.onboarding_seed_information, R.drawable.ic_onboarding_seed, true),
|
||||
FPSEND(R.string.onboarding_fpsend_title, R.string.onboarding_fpsend_information, R.drawable.ic_onboarding_fingerprint, false),
|
||||
XMRTO(R.string.onboarding_xmrto_title, R.string.onboarding_xmrto_information, R.drawable.ic_onboarding_xmrto, false),
|
||||
NODES(R.string.onboarding_nodes_title, R.string.onboarding_nodes_information, R.drawable.ic_onboarding_nodes, false);
|
||||
|
||||
private final int title;
|
||||
private final int information;
|
||||
private final int drawable;
|
||||
private boolean mustAgree;
|
||||
|
||||
OnBoardingScreen(final int title, final int information, final int drawable, final boolean mustAgree) {
|
||||
this.title = title;
|
||||
this.information = information;
|
||||
this.drawable = drawable;
|
||||
this.mustAgree = mustAgree;
|
||||
}
|
||||
|
||||
public int getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public int getInformation() {
|
||||
return information;
|
||||
}
|
||||
|
||||
public int getDrawable() {
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public boolean isMustAgree() {
|
||||
return mustAgree;
|
||||
}
|
||||
|
||||
public boolean setMustAgree(boolean mustAgree) {
|
||||
return this.mustAgree = mustAgree;
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2020 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.
|
||||
*/
|
||||
// based on https://stackoverflow.com/a/34076649
|
||||
|
||||
package com.m2049r.xmrwallet.onboarding;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
public class OnBoardingViewPager extends ViewPager {
|
||||
|
||||
public enum SwipeDirection {
|
||||
ALL, LEFT, RIGHT, NONE;
|
||||
}
|
||||
|
||||
private float initialXValue;
|
||||
private SwipeDirection direction;
|
||||
|
||||
public OnBoardingViewPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.direction = SwipeDirection.ALL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (this.IsSwipeAllowed(event)) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
if (this.IsSwipeAllowed(event)) {
|
||||
return super.onInterceptTouchEvent(event);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean IsSwipeAllowed(MotionEvent event) {
|
||||
if (this.direction == SwipeDirection.ALL) return true;
|
||||
|
||||
if (direction == SwipeDirection.NONE)//disable any swipe
|
||||
return false;
|
||||
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
initialXValue = event.getX();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
float diffX = event.getX() - initialXValue;
|
||||
if (diffX > 0 && direction == SwipeDirection.RIGHT) {
|
||||
// swipe from left to right detected
|
||||
return false;
|
||||
} else if (diffX < 0 && direction == SwipeDirection.LEFT) {
|
||||
// swipe from right to left detected
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setAllowedSwipeDirection(SwipeDirection direction) {
|
||||
this.direction = direction;
|
||||
}
|
||||
}
|
@ -22,140 +22,85 @@ import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.m2049r.xmrwallet.data.DefaultNodes;
|
||||
import com.m2049r.xmrwallet.data.Node;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.fragment.home.HomeViewModel;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletListener;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
|
||||
/**
|
||||
* Handy class for starting a new thread that has a looper. The looper can then be
|
||||
* used to create handler classes. Note that start() must still be called.
|
||||
* The started Thread has a stck size of STACK_SIZE (=5MB)
|
||||
*/
|
||||
public class MoneroHandlerThread extends Thread {
|
||||
public class MoneroHandlerThread extends Thread implements WalletListener {
|
||||
private Listener listener = null;
|
||||
private Wallet wallet = null;
|
||||
// from src/cryptonote_config.h
|
||||
static public final long THREAD_STACK_SIZE = 5 * 1024 * 1024;
|
||||
private int mPriority;
|
||||
private int mTid = -1;
|
||||
private Looper mLooper;
|
||||
|
||||
public MoneroHandlerThread(String name) {
|
||||
public MoneroHandlerThread(String name, Wallet wallet, Listener listener) {
|
||||
super(null, null, name, THREAD_STACK_SIZE);
|
||||
mPriority = Process.THREAD_PRIORITY_DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a MoneroHandlerThread.
|
||||
*
|
||||
* @param name
|
||||
* @param priority The priority to run the thread at. The value supplied must be from
|
||||
* {@link android.os.Process} and not from java.lang.Thread.
|
||||
*/
|
||||
MoneroHandlerThread(String name, int priority) {
|
||||
super(null, null, name, THREAD_STACK_SIZE);
|
||||
mPriority = priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call back method that can be explicitly overridden if needed to execute some
|
||||
* setup before Looper loops.
|
||||
*/
|
||||
|
||||
private void onLooperPrepared() {
|
||||
this.wallet = wallet;
|
||||
this.listener = listener;
|
||||
this.listener.onRefresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mTid = Process.myTid();
|
||||
Looper.prepare();
|
||||
synchronized (this) {
|
||||
mLooper = Looper.myLooper();
|
||||
notifyAll();
|
||||
}
|
||||
Process.setThreadPriority(mPriority);
|
||||
onLooperPrepared();
|
||||
Looper.loop();
|
||||
mTid = -1;
|
||||
WalletManager.getInstance().setDaemon(Node.fromString(DefaultNodes.XMRTW.getUri()));
|
||||
System.out.println(WalletManager.getInstance().getBlockchainHeight());
|
||||
System.out.println(wallet.getSeed(""));
|
||||
wallet.init(0);
|
||||
wallet.setListener(this);
|
||||
wallet.startRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the Looper associated with this thread. If this thread not been started
|
||||
* or for any reason is isAlive() returns false, this method will return null. If this thread
|
||||
* has been started, this method will block until the looper has been initialized.
|
||||
*
|
||||
* @return The looper.
|
||||
*/
|
||||
Looper getLooper() {
|
||||
if (!isAlive()) {
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public void moneySpent(String txId, long amount) {}
|
||||
@Override
|
||||
public void moneyReceived(String txId, long amount) {}
|
||||
@Override
|
||||
public void unconfirmedMoneyReceived(String txId, long amount) {}
|
||||
|
||||
// If the thread has been started, wait until the looper has been created.
|
||||
synchronized (this) {
|
||||
while (isAlive() && mLooper == null) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void newBlock(long height) {
|
||||
if(height % 1000 == 0) {
|
||||
refresh();
|
||||
}
|
||||
return mLooper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quits the handler thread's looper.
|
||||
* <p>
|
||||
* Causes the handler thread's looper to terminate without processing any
|
||||
* more messages in the message queue.
|
||||
* </p><p>
|
||||
* Any attempt to post messages to the queue after the looper is asked to quit will fail.
|
||||
* For example, the {@link Handler#sendMessage(Message)} method will return false.
|
||||
* </p><p class="note">
|
||||
* Using this method may be unsafe because some messages may not be delivered
|
||||
* before the looper terminates. Consider using {@link #quitSafely} instead to ensure
|
||||
* that all pending work is completed in an orderly manner.
|
||||
* </p>
|
||||
*
|
||||
* @return True if the looper looper has been asked to quit or false if the
|
||||
* thread had not yet started running.
|
||||
* @see #quitSafely
|
||||
*/
|
||||
public boolean quit() {
|
||||
Looper looper = getLooper();
|
||||
if (looper != null) {
|
||||
looper.quit();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@Override
|
||||
public void updated() {
|
||||
refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Quits the handler thread's looper safely.
|
||||
* <p>
|
||||
* Causes the handler thread's looper to terminate as soon as all remaining messages
|
||||
* in the message queue that are already due to be delivered have been handled.
|
||||
* Pending delayed messages with due times in the future will not be delivered.
|
||||
* </p><p>
|
||||
* Any attempt to post messages to the queue after the looper is asked to quit will fail.
|
||||
* For example, the {@link Handler#sendMessage(Message)} method will return false.
|
||||
* </p><p>
|
||||
* If the thread has not been started or has finished (that is if
|
||||
* {@link #getLooper} returns null), then false is returned.
|
||||
* Otherwise the looper is asked to quit and true is returned.
|
||||
* </p>
|
||||
*
|
||||
* @return True if the looper looper has been asked to quit or false if the
|
||||
* thread had not yet started running.
|
||||
*/
|
||||
public boolean quitSafely() {
|
||||
Looper looper = getLooper();
|
||||
if (looper != null) {
|
||||
looper.quitSafely();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@Override
|
||||
public void refreshed() {
|
||||
wallet.setSynchronized();
|
||||
refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier of this thread. See Process.myTid().
|
||||
*/
|
||||
public int getThreadId() {
|
||||
return mTid;
|
||||
private void refresh() {
|
||||
wallet.refreshHistory();
|
||||
wallet.store();
|
||||
listener.onRefresh();
|
||||
}
|
||||
|
||||
public boolean sendTx(String address, String amountStr) {
|
||||
long amount = Wallet.getAmountFromString(amountStr);
|
||||
PendingTransaction pendingTx = wallet.createTransaction(new TxData(address, amount, 0, PendingTransaction.Priority.Priority_Default));
|
||||
return pendingTx.commit("", true);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRefresh();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
package com.m2049r.xmrwallet.service;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
|
||||
public class ServiceBase {
|
||||
private MainActivity mainActivity;
|
||||
private MoneroHandlerThread thread;
|
||||
|
||||
public ServiceBase(MainActivity mainActivity, MoneroHandlerThread thread) {
|
||||
this.mainActivity = mainActivity;
|
||||
this.thread = thread;
|
||||
}
|
||||
|
||||
public MainActivity getMainActivity() {
|
||||
return mainActivity;
|
||||
}
|
||||
|
||||
public MoneroHandlerThread getThread() {
|
||||
return thread;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.m2049r.xmrwallet.service;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.livedata.SingleLiveEvent;
|
||||
|
||||
public class TxService extends ServiceBase {
|
||||
public static TxService instance = null;
|
||||
public static TxService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final SingleLiveEvent _clearSendEvent = new SingleLiveEvent();
|
||||
public SingleLiveEvent clearSendEvent = _clearSendEvent;
|
||||
|
||||
public TxService(MainActivity mainActivity, MoneroHandlerThread thread) {
|
||||
super(mainActivity, thread);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public void sendTx(String address, String amount) {
|
||||
boolean success = this.getThread().sendTx(address, amount);
|
||||
if(success) {
|
||||
_clearSendEvent.call();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,595 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.WalletActivity;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletListener;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.LocaleHelper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class WalletService extends Service {
|
||||
public static boolean Running = false;
|
||||
|
||||
final static int NOTIFICATION_ID = 2049;
|
||||
final static String CHANNEL_ID = "m_service";
|
||||
|
||||
public static final String REQUEST_WALLET = "wallet";
|
||||
public static final String REQUEST = "request";
|
||||
|
||||
public static final String REQUEST_CMD_LOAD = "load";
|
||||
public static final String REQUEST_CMD_LOAD_PW = "walletPassword";
|
||||
|
||||
public static final String REQUEST_CMD_STORE = "store";
|
||||
|
||||
public static final String REQUEST_CMD_TX = "createTX";
|
||||
public static final String REQUEST_CMD_TX_DATA = "data";
|
||||
public static final String REQUEST_CMD_TX_TAG = "tag";
|
||||
|
||||
public static final String REQUEST_CMD_SWEEP = "sweepTX";
|
||||
|
||||
public static final String REQUEST_CMD_SEND = "send";
|
||||
public static final String REQUEST_CMD_SEND_NOTES = "notes";
|
||||
|
||||
public static final int START_SERVICE = 1;
|
||||
public static final int STOP_SERVICE = 2;
|
||||
|
||||
private MyWalletListener listener = null;
|
||||
|
||||
private class MyWalletListener implements WalletListener {
|
||||
boolean updated = true;
|
||||
|
||||
void start() {
|
||||
Timber.d("MyWalletListener.start()");
|
||||
Wallet wallet = getWallet();
|
||||
if (wallet == null) throw new IllegalStateException("No wallet!");
|
||||
wallet.setListener(this);
|
||||
wallet.startRefresh();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
Timber.d("MyWalletListener.stop()");
|
||||
Wallet wallet = getWallet();
|
||||
if (wallet == null) throw new IllegalStateException("No wallet!");
|
||||
wallet.pauseRefresh();
|
||||
wallet.setListener(null);
|
||||
}
|
||||
|
||||
// WalletListener callbacks
|
||||
public void moneySpent(String txId, long amount) {
|
||||
Timber.d("moneySpent() %d @ %s", amount, txId);
|
||||
}
|
||||
|
||||
public void moneyReceived(String txId, long amount) {
|
||||
Timber.d("moneyReceived() %d @ %s", amount, txId);
|
||||
}
|
||||
|
||||
public void unconfirmedMoneyReceived(String txId, long amount) {
|
||||
Timber.d("unconfirmedMoneyReceived() %d @ %s", amount, txId);
|
||||
}
|
||||
|
||||
private long lastBlockTime = 0;
|
||||
private int lastTxCount = 0;
|
||||
|
||||
public void newBlock(long height) {
|
||||
final Wallet wallet = getWallet();
|
||||
if (wallet == null) throw new IllegalStateException("No wallet!");
|
||||
// don't flood with an update for every block ...
|
||||
if (lastBlockTime < System.currentTimeMillis() - 2000) {
|
||||
lastBlockTime = System.currentTimeMillis();
|
||||
Timber.d("newBlock() @ %d with observer %s", height, observer);
|
||||
if (observer != null) {
|
||||
boolean fullRefresh = false;
|
||||
updateDaemonState(wallet, wallet.isSynchronized() ? height : 0);
|
||||
if (!wallet.isSynchronized()) {
|
||||
updated = true;
|
||||
// we want to see our transactions as they come in
|
||||
wallet.refreshHistory();
|
||||
int txCount = wallet.getHistory().getCount();
|
||||
if (txCount > lastTxCount) {
|
||||
// update the transaction list only if we have more than before
|
||||
lastTxCount = txCount;
|
||||
fullRefresh = true;
|
||||
}
|
||||
}
|
||||
if (observer != null)
|
||||
observer.onRefreshed(wallet, fullRefresh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updated() {
|
||||
Timber.d("updated()");
|
||||
Wallet wallet = getWallet();
|
||||
if (wallet == null) throw new IllegalStateException("No wallet!");
|
||||
updated = true;
|
||||
}
|
||||
|
||||
public void refreshed() { // this means it's synced
|
||||
Timber.d("refreshed()");
|
||||
final Wallet wallet = getWallet();
|
||||
if (wallet == null) throw new IllegalStateException("No wallet!");
|
||||
wallet.setSynchronized();
|
||||
if (updated) {
|
||||
updateDaemonState(wallet, wallet.getBlockChainHeight());
|
||||
wallet.refreshHistory();
|
||||
if (observer != null) {
|
||||
updated = !observer.onRefreshed(wallet, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long lastDaemonStatusUpdate = 0;
|
||||
private long daemonHeight = 0;
|
||||
private Wallet.ConnectionStatus connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Disconnected;
|
||||
private static final long STATUS_UPDATE_INTERVAL = 120000; // 120s (blocktime)
|
||||
|
||||
private void updateDaemonState(Wallet wallet, long height) {
|
||||
long t = System.currentTimeMillis();
|
||||
if (height > 0) { // if we get a height, we are connected
|
||||
daemonHeight = height;
|
||||
connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Connected;
|
||||
lastDaemonStatusUpdate = t;
|
||||
} else {
|
||||
if (t - lastDaemonStatusUpdate > STATUS_UPDATE_INTERVAL) {
|
||||
lastDaemonStatusUpdate = t;
|
||||
// these calls really connect to the daemon - wasting time
|
||||
daemonHeight = wallet.getDaemonBlockChainHeight();
|
||||
if (daemonHeight > 0) {
|
||||
// if we get a valid height, then obviously we are connected
|
||||
connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Connected;
|
||||
} else {
|
||||
connectionStatus = Wallet.ConnectionStatus.ConnectionStatus_Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long getDaemonHeight() {
|
||||
return this.daemonHeight;
|
||||
}
|
||||
|
||||
public Wallet.ConnectionStatus getConnectionStatus() {
|
||||
return this.connectionStatus;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// communication back to client (activity) //
|
||||
/////////////////////////////////////////////
|
||||
// NB: This allows for only one observer, i.e. only a single activity bound here
|
||||
|
||||
private Observer observer = null;
|
||||
|
||||
public void setObserver(Observer anObserver) {
|
||||
observer = anObserver;
|
||||
Timber.d("setObserver %s", observer);
|
||||
}
|
||||
|
||||
public interface Observer {
|
||||
boolean onRefreshed(Wallet wallet, boolean full);
|
||||
|
||||
void onProgress(String text);
|
||||
|
||||
void onProgress(int n);
|
||||
|
||||
void onWalletStored(boolean success);
|
||||
|
||||
void onTransactionCreated(String tag, PendingTransaction pendingTransaction);
|
||||
|
||||
void onTransactionSent(String txid);
|
||||
|
||||
void onSendTransactionFailed(String error);
|
||||
|
||||
void onWalletStarted(Wallet.Status walletStatus);
|
||||
|
||||
void onWalletOpen(Wallet.Device device);
|
||||
}
|
||||
|
||||
String progressText = null;
|
||||
int progressValue = -1;
|
||||
|
||||
private void showProgress(String text) {
|
||||
progressText = text;
|
||||
if (observer != null) {
|
||||
observer.onProgress(text);
|
||||
}
|
||||
}
|
||||
|
||||
private void showProgress(int n) {
|
||||
progressValue = n;
|
||||
if (observer != null) {
|
||||
observer.onProgress(n);
|
||||
}
|
||||
}
|
||||
|
||||
public String getProgressText() {
|
||||
return progressText;
|
||||
}
|
||||
|
||||
public int getProgressValue() {
|
||||
return progressValue;
|
||||
}
|
||||
|
||||
//
|
||||
public Wallet getWallet() {
|
||||
return WalletManager.getInstance().getWallet();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
/////////////////////////////////////////////
|
||||
|
||||
private WalletService.ServiceHandler mServiceHandler;
|
||||
|
||||
private boolean errorState = false;
|
||||
|
||||
// Handler that receives messages from the thread
|
||||
private final class ServiceHandler extends Handler {
|
||||
ServiceHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Timber.d("Handling %s", msg.arg2);
|
||||
if (errorState) {
|
||||
Timber.i("In error state.");
|
||||
// also, we have already stopped ourselves
|
||||
return;
|
||||
}
|
||||
switch (msg.arg2) {
|
||||
case START_SERVICE: {
|
||||
Bundle extras = msg.getData();
|
||||
String cmd = extras.getString(REQUEST, null);
|
||||
switch (cmd) {
|
||||
case REQUEST_CMD_LOAD:
|
||||
String walletId = extras.getString(REQUEST_WALLET, null);
|
||||
String walletPw = extras.getString(REQUEST_CMD_LOAD_PW, null);
|
||||
Timber.d("LOAD wallet %s", walletId);
|
||||
if (walletId != null) {
|
||||
showProgress(getString(R.string.status_wallet_loading));
|
||||
showProgress(10);
|
||||
Wallet.Status walletStatus = start(walletId, walletPw);
|
||||
if (observer != null) observer.onWalletStarted(walletStatus);
|
||||
if ((walletStatus == null) || !walletStatus.isOk()) {
|
||||
errorState = true;
|
||||
stop();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case REQUEST_CMD_STORE: {
|
||||
Wallet myWallet = getWallet();
|
||||
if (myWallet == null) break;
|
||||
Timber.d("STORE wallet: %s", myWallet.getName());
|
||||
boolean rc = myWallet.store();
|
||||
Timber.d("wallet stored: %s with rc=%b", myWallet.getName(), rc);
|
||||
if (!rc) {
|
||||
Timber.w("Wallet store failed: %s", myWallet.getStatus().getErrorString());
|
||||
}
|
||||
if (observer != null) observer.onWalletStored(rc);
|
||||
break;
|
||||
}
|
||||
case REQUEST_CMD_TX: {
|
||||
Wallet myWallet = getWallet();
|
||||
if (myWallet == null) break;
|
||||
Timber.d("CREATE TX for wallet: %s", myWallet.getName());
|
||||
myWallet.disposePendingTransaction(); // remove any old pending tx
|
||||
|
||||
TxData txData = extras.getParcelable(REQUEST_CMD_TX_DATA);
|
||||
String txTag = extras.getString(REQUEST_CMD_TX_TAG);
|
||||
PendingTransaction pendingTransaction = myWallet.createTransaction(txData);
|
||||
PendingTransaction.Status status = pendingTransaction.getStatus();
|
||||
Timber.d("transaction status %s", status);
|
||||
if (status != PendingTransaction.Status.Status_Ok) {
|
||||
Timber.w("Create Transaction failed: %s", pendingTransaction.getErrorString());
|
||||
}
|
||||
if (observer != null) {
|
||||
observer.onTransactionCreated(txTag, pendingTransaction);
|
||||
} else {
|
||||
myWallet.disposePendingTransaction();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case REQUEST_CMD_SWEEP: {
|
||||
Wallet myWallet = getWallet();
|
||||
if (myWallet == null) break;
|
||||
Timber.d("SWEEP TX for wallet: %s", myWallet.getName());
|
||||
myWallet.disposePendingTransaction(); // remove any old pending tx
|
||||
|
||||
String txTag = extras.getString(REQUEST_CMD_TX_TAG);
|
||||
PendingTransaction pendingTransaction = myWallet.createSweepUnmixableTransaction();
|
||||
PendingTransaction.Status status = pendingTransaction.getStatus();
|
||||
Timber.d("transaction status %s", status);
|
||||
if (status != PendingTransaction.Status.Status_Ok) {
|
||||
Timber.w("Create Transaction failed: %s", pendingTransaction.getErrorString());
|
||||
}
|
||||
if (observer != null) {
|
||||
observer.onTransactionCreated(txTag, pendingTransaction);
|
||||
} else {
|
||||
myWallet.disposePendingTransaction();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case REQUEST_CMD_SEND: {
|
||||
Wallet myWallet = getWallet();
|
||||
if (myWallet == null) break;
|
||||
Timber.d("SEND TX for wallet: %s", myWallet.getName());
|
||||
PendingTransaction pendingTransaction = myWallet.getPendingTransaction();
|
||||
if (pendingTransaction == null) {
|
||||
throw new IllegalArgumentException("PendingTransaction is null"); // die
|
||||
}
|
||||
if (pendingTransaction.getStatus() != PendingTransaction.Status.Status_Ok) {
|
||||
Timber.e("PendingTransaction is %s", pendingTransaction.getStatus());
|
||||
final String error = pendingTransaction.getErrorString();
|
||||
myWallet.disposePendingTransaction(); // it's broken anyway
|
||||
if (observer != null) observer.onSendTransactionFailed(error);
|
||||
return;
|
||||
}
|
||||
final String txid = pendingTransaction.getFirstTxId(); // tx ids vanish after commit()!
|
||||
|
||||
boolean success = pendingTransaction.commit("", true);
|
||||
if (success) {
|
||||
myWallet.disposePendingTransaction();
|
||||
if (observer != null) observer.onTransactionSent(txid);
|
||||
String notes = extras.getString(REQUEST_CMD_SEND_NOTES);
|
||||
if ((notes != null) && (!notes.isEmpty())) {
|
||||
myWallet.setUserNote(txid, notes);
|
||||
}
|
||||
boolean rc = myWallet.store();
|
||||
Timber.d("wallet stored: %s with rc=%b", myWallet.getName(), rc);
|
||||
if (!rc) {
|
||||
Timber.w("Wallet store failed: %s", myWallet.getStatus().getErrorString());
|
||||
}
|
||||
if (observer != null) observer.onWalletStored(rc);
|
||||
listener.updated = true;
|
||||
} else {
|
||||
final String error = pendingTransaction.getErrorString();
|
||||
myWallet.disposePendingTransaction();
|
||||
if (observer != null) observer.onSendTransactionFailed(error);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case STOP_SERVICE:
|
||||
stop();
|
||||
break;
|
||||
default:
|
||||
Timber.e("UNKNOWN %s", msg.arg2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// We are using a HandlerThread and a Looper to avoid loading and closing
|
||||
// concurrency
|
||||
MoneroHandlerThread thread = new MoneroHandlerThread("WalletService",
|
||||
Process.THREAD_PRIORITY_BACKGROUND);
|
||||
thread.start();
|
||||
|
||||
// Get the HandlerThread's Looper and use it for our Handler
|
||||
final Looper serviceLooper = thread.getLooper();
|
||||
mServiceHandler = new WalletService.ServiceHandler(serviceLooper);
|
||||
|
||||
Timber.d("Service created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Timber.d("onDestroy()");
|
||||
if (this.listener != null) {
|
||||
Timber.w("onDestroy() with active listener");
|
||||
// no need to stop() here because the wallet closing should have been triggered
|
||||
// through onUnbind() already
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context context) {
|
||||
super.attachBaseContext(LocaleHelper.setPreferredLocale(context));
|
||||
}
|
||||
|
||||
public class WalletServiceBinder extends Binder {
|
||||
public WalletService getService() {
|
||||
return WalletService.this;
|
||||
}
|
||||
}
|
||||
|
||||
private final IBinder mBinder = new WalletServiceBinder();
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Running = true;
|
||||
// when the activity starts the service, it expects to start it for a new wallet
|
||||
// the service is possibly still occupied with saving the last opened wallet
|
||||
// so we queue the open request
|
||||
// this should not matter since the old activity is not getting updates
|
||||
// and the new one is not listening yet (although it will be bound)
|
||||
Timber.d("onStartCommand()");
|
||||
// For each start request, send a message to start a job and deliver the
|
||||
// start ID so we know which request we're stopping when we finish the job
|
||||
Message msg = mServiceHandler.obtainMessage();
|
||||
msg.arg2 = START_SERVICE;
|
||||
if (intent != null) {
|
||||
msg.setData(intent.getExtras());
|
||||
mServiceHandler.sendMessage(msg);
|
||||
return START_STICKY;
|
||||
} else {
|
||||
// process restart - don't do anything - let system kill it again
|
||||
stop();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// Very first client binds
|
||||
Timber.d("onBind()");
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
Timber.d("onUnbind()");
|
||||
// All clients have unbound with unbindService()
|
||||
Message msg = mServiceHandler.obtainMessage();
|
||||
msg.arg2 = STOP_SERVICE;
|
||||
mServiceHandler.sendMessage(msg);
|
||||
Timber.d("onUnbind() message sent");
|
||||
return true; // true is important so that onUnbind is also called next time
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Wallet.Status start(String walletName, String walletPassword) {
|
||||
Timber.d("start()");
|
||||
startNotfication();
|
||||
showProgress(getString(R.string.status_wallet_loading));
|
||||
showProgress(10);
|
||||
if (listener == null) {
|
||||
Timber.d("start() loadWallet");
|
||||
Wallet aWallet = loadWallet(walletName, walletPassword);
|
||||
if (aWallet == null) return null;
|
||||
Wallet.Status walletStatus = aWallet.getFullStatus();
|
||||
if (!walletStatus.isOk()) {
|
||||
aWallet.close();
|
||||
return walletStatus;
|
||||
}
|
||||
listener = new MyWalletListener();
|
||||
listener.start();
|
||||
showProgress(100);
|
||||
}
|
||||
showProgress(getString(R.string.status_wallet_connecting));
|
||||
showProgress(101);
|
||||
// if we try to refresh the history here we get occasional segfaults!
|
||||
// doesnt matter since we update as soon as we get a new block anyway
|
||||
Timber.d("start() done");
|
||||
return getWallet().getFullStatus();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
Timber.d("stop()");
|
||||
setObserver(null); // in case it was not reset already
|
||||
if (listener != null) {
|
||||
listener.stop();
|
||||
Wallet myWallet = getWallet();
|
||||
Timber.d("stop() closing");
|
||||
myWallet.close();
|
||||
Timber.d("stop() closed");
|
||||
listener = null;
|
||||
}
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
Running = false;
|
||||
}
|
||||
|
||||
private Wallet loadWallet(String walletName, String walletPassword) {
|
||||
Wallet wallet = openWallet(walletName, walletPassword);
|
||||
if (wallet != null) {
|
||||
Timber.d("Using daemon %s", WalletManager.getInstance().getDaemonAddress());
|
||||
showProgress(55);
|
||||
wallet.init(0);
|
||||
wallet.setProxy(NetCipherHelper.getProxy());
|
||||
showProgress(90);
|
||||
}
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private Wallet openWallet(String walletName, String walletPassword) {
|
||||
String path = Helper.getWalletFile(getApplicationContext(), walletName).getAbsolutePath();
|
||||
showProgress(20);
|
||||
Wallet wallet = null;
|
||||
WalletManager walletMgr = WalletManager.getInstance();
|
||||
Timber.d("WalletManager network=%s", walletMgr.getNetworkType().name());
|
||||
showProgress(30);
|
||||
if (walletMgr.walletExists(path)) {
|
||||
Timber.d("open wallet %s", path);
|
||||
Wallet.Device device = WalletManager.getInstance().queryWalletDevice(path + ".keys", walletPassword);
|
||||
Timber.d("device is %s", device.toString());
|
||||
if (observer != null) observer.onWalletOpen(device);
|
||||
wallet = walletMgr.openWallet(path, walletPassword);
|
||||
showProgress(60);
|
||||
Timber.d("wallet opened");
|
||||
Wallet.Status walletStatus = wallet.getStatus();
|
||||
if (!walletStatus.isOk()) {
|
||||
Timber.d("wallet status is %s", walletStatus);
|
||||
WalletManager.getInstance().close(wallet); // TODO close() failed?
|
||||
wallet = null;
|
||||
// TODO what do we do with the progress??
|
||||
// TODO tell the activity this failed
|
||||
// this crashes in MyWalletListener(Wallet aWallet) as wallet == null
|
||||
}
|
||||
}
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private void startNotfication() {
|
||||
Intent notificationIntent = new Intent(this, WalletActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
|
||||
String channelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? createNotificationChannel() : "";
|
||||
Notification notification = new NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle(getString(R.string.service_description))
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_monerujo)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build();
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private String createNotificationChannel() {
|
||||
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, getString(R.string.service_description),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
return CHANNEL_ID;
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2006 The Android Open Source Project
|
||||
* Copyright (c) 2017 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.exchange.api;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
public interface ExchangeApi {
|
||||
|
||||
/**
|
||||
* Queries the exchnage rate
|
||||
*
|
||||
* @param baseCurrency base currency
|
||||
* @param quoteCurrency quote currency
|
||||
* @param callback the callback with the exchange rate
|
||||
*/
|
||||
void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
|
||||
@NonNull final ExchangeCallback callback);
|
||||
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2006 The Android Open Source Project
|
||||
* Copyright (c) 2017 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.exchange.api;
|
||||
|
||||
public interface ExchangeCallback {
|
||||
|
||||
void onSuccess(ExchangeRate exchangeRate);
|
||||
|
||||
void onError(Exception ex);
|
||||
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.exchange.api;
|
||||
|
||||
public class ExchangeException extends Exception {
|
||||
private final int code;
|
||||
private final String errorMsg;
|
||||
|
||||
public String getErrorMsg() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
public ExchangeException(final int code) {
|
||||
super();
|
||||
this.code = code;
|
||||
this.errorMsg = null;
|
||||
}
|
||||
|
||||
public ExchangeException(final String errorMsg) {
|
||||
super();
|
||||
this.code = 0;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public ExchangeException(final int code, final String errorMsg) {
|
||||
super();
|
||||
this.code = code;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.exchange.api;
|
||||
|
||||
public interface ExchangeRate {
|
||||
|
||||
String getServiceName();
|
||||
|
||||
String getBaseCurrency();
|
||||
|
||||
String getQuoteCurrency();
|
||||
|
||||
double getRate();
|
||||
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 m2049r@monerujo.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://developer.android.com/training/basics/network-ops/xml
|
||||
|
||||
package com.m2049r.xmrwallet.service.exchange.ecb;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ExchangeApiImpl implements ExchangeApi {
|
||||
@NonNull
|
||||
private final HttpUrl baseUrl;
|
||||
|
||||
//so we can inject the mockserver url
|
||||
@VisibleForTesting
|
||||
public ExchangeApiImpl(@NonNull final HttpUrl baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public ExchangeApiImpl() {
|
||||
this(HttpUrl.parse("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"));
|
||||
// data is daily and is refreshed around 16:00 CET every working day
|
||||
}
|
||||
|
||||
public static boolean isSameDay(Calendar calendar, Calendar anotherCalendar) {
|
||||
return (calendar.get(Calendar.YEAR) == anotherCalendar.get(Calendar.YEAR)) &&
|
||||
(calendar.get(Calendar.DAY_OF_YEAR) == anotherCalendar.get(Calendar.DAY_OF_YEAR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
|
||||
@NonNull final ExchangeCallback callback) {
|
||||
if (!baseCurrency.equals("EUR")) {
|
||||
callback.onError(new IllegalArgumentException("Only EUR supported as base"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (baseCurrency.equals(quoteCurrency)) {
|
||||
callback.onSuccess(new ExchangeRateImpl(quoteCurrency, 1.0, new Date()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (fetchDate != null) { // we have data
|
||||
boolean useCache = false;
|
||||
// figure out if we can use the cached values
|
||||
// data is daily and is refreshed around 16:00 CET every working day
|
||||
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("CET"));
|
||||
|
||||
int fetchWeekday = fetchDate.get(Calendar.DAY_OF_WEEK);
|
||||
int fetchDay = fetchDate.get(Calendar.DAY_OF_YEAR);
|
||||
int fetchHour = fetchDate.get(Calendar.HOUR_OF_DAY);
|
||||
|
||||
int today = now.get(Calendar.DAY_OF_YEAR);
|
||||
int nowHour = now.get(Calendar.HOUR_OF_DAY);
|
||||
|
||||
if (
|
||||
// was it fetched today before 16:00? assume no new data iff now < 16:00 as well
|
||||
((today == fetchDay) && (fetchHour < 16) && (nowHour < 16))
|
||||
// was it fetched after, 17:00? we can assume there is no newer data
|
||||
|| ((today == fetchDay) && (fetchHour > 17))
|
||||
|| ((today == fetchDay + 1) && (fetchHour > 17) && (nowHour < 16))
|
||||
// is the data itself from today? there can be no newer data
|
||||
|| (fxDate.get(Calendar.DAY_OF_YEAR) == today)
|
||||
// was it fetched Sat/Sun? we can assume there is no newer data
|
||||
|| ((fetchWeekday == Calendar.SATURDAY) || (fetchWeekday == Calendar.SUNDAY))
|
||||
) { // return cached rate
|
||||
try {
|
||||
callback.onSuccess(getRate(quoteCurrency));
|
||||
} catch (ExchangeException ex) {
|
||||
callback.onError(ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(baseUrl);
|
||||
httpRequest.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(final Call call, final IOException ex) {
|
||||
callback.onError(ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(final Call call, final Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
try {
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
Document doc = dBuilder.parse(response.body().byteStream());
|
||||
doc.getDocumentElement().normalize();
|
||||
parse(doc);
|
||||
try {
|
||||
callback.onSuccess(getRate(quoteCurrency));
|
||||
} catch (ExchangeException ex) {
|
||||
callback.onError(ex);
|
||||
}
|
||||
} catch (ParserConfigurationException | SAXException ex) {
|
||||
Timber.w(ex);
|
||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||
}
|
||||
} else {
|
||||
callback.onError(new ExchangeException(response.code(), response.message()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final private Map<String, Double> fxEntries = new HashMap<>();
|
||||
private Calendar fxDate = null;
|
||||
private Calendar fetchDate = null;
|
||||
|
||||
synchronized private ExchangeRate getRate(String currency) throws ExchangeException {
|
||||
Timber.d("Getting %s", currency);
|
||||
final Double rate = fxEntries.get(currency);
|
||||
if (rate == null) throw new ExchangeException(404, "Currency not supported: " + currency);
|
||||
return new ExchangeRateImpl(currency, rate, fxDate.getTime());
|
||||
}
|
||||
|
||||
private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||
|
||||
{
|
||||
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private void parse(final Document xmlRootDoc) {
|
||||
final Map<String, Double> entries = new HashMap<>();
|
||||
Calendar date = Calendar.getInstance(TimeZone.getTimeZone("CET"));
|
||||
try {
|
||||
NodeList cubes = xmlRootDoc.getElementsByTagName("Cube");
|
||||
for (int i = 0; i < cubes.getLength(); i++) {
|
||||
Node node = cubes.item(i);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
Element cube = (Element) node;
|
||||
if (cube.hasAttribute("time")) { // a time Cube
|
||||
final Date time = DATE_FORMAT.parse(cube.getAttribute("time"));
|
||||
date.setTime(time);
|
||||
} else if (cube.hasAttribute("currency")
|
||||
&& cube.hasAttribute("rate")) { // a rate Cube
|
||||
String currency = cube.getAttribute("currency");
|
||||
double rate = Double.valueOf(cube.getAttribute("rate"));
|
||||
entries.put(currency, rate);
|
||||
} // else an empty Cube - ignore
|
||||
}
|
||||
}
|
||||
} catch (ParseException ex) {
|
||||
Timber.d(ex);
|
||||
}
|
||||
synchronized (this) {
|
||||
if (date != null) {
|
||||
fetchDate = Calendar.getInstance(TimeZone.getTimeZone("CET"));
|
||||
fxDate = date;
|
||||
fxEntries.clear();
|
||||
fxEntries.putAll(entries);
|
||||
}
|
||||
// else don't change what we have
|
||||
}
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 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.exchange.ecb;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
class ExchangeRateImpl implements ExchangeRate {
|
||||
private final Date date;
|
||||
private final String baseCurrency = "EUR";
|
||||
private final String quoteCurrency;
|
||||
private final double rate;
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "ecb.europa.eu";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseCurrency() {
|
||||
return baseCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuoteCurrency() {
|
||||
return quoteCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getRate() {
|
||||
return rate;
|
||||
}
|
||||
|
||||
ExchangeRateImpl(@NonNull final String quoteCurrency, double rate, @NonNull final Date date) {
|
||||
super();
|
||||
this.quoteCurrency = quoteCurrency;
|
||||
this.rate = rate;
|
||||
this.date = date;
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017-2019 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.exchange.kraken;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import org.json.JSONArray;
|
||||
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 ExchangeApiImpl implements ExchangeApi {
|
||||
|
||||
private final HttpUrl baseUrl;
|
||||
|
||||
//so we can inject the mockserver url
|
||||
@VisibleForTesting
|
||||
public ExchangeApiImpl(final HttpUrl baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public ExchangeApiImpl() {
|
||||
this(HttpUrl.parse("https://api.kraken.com/0/public/Ticker"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
|
||||
@NonNull final ExchangeCallback callback) {
|
||||
|
||||
if (baseCurrency.equals(quoteCurrency)) {
|
||||
callback.onSuccess(new ExchangeRateImpl(baseCurrency, quoteCurrency, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean invertQuery;
|
||||
|
||||
|
||||
if (Helper.BASE_CRYPTO.equals(baseCurrency)) {
|
||||
invertQuery = false;
|
||||
} else if (Helper.BASE_CRYPTO.equals(quoteCurrency)) {
|
||||
invertQuery = true;
|
||||
} else {
|
||||
callback.onError(new IllegalArgumentException("no crypto specified"));
|
||||
return;
|
||||
}
|
||||
|
||||
Timber.d("queryExchangeRate: i %b, b %s, q %s", invertQuery, baseCurrency, quoteCurrency);
|
||||
final boolean invert = invertQuery;
|
||||
final String base = invert ? quoteCurrency : baseCurrency;
|
||||
final String quote = invert ? baseCurrency : quoteCurrency;
|
||||
|
||||
final HttpUrl url = baseUrl.newBuilder()
|
||||
.addQueryParameter("pair", base + (quote.equals("BTC") ? "XBT" : quote))
|
||||
.build();
|
||||
|
||||
final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url);
|
||||
httpRequest.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(final Call call, final IOException ex) {
|
||||
callback.onError(ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(final Call call, final Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
try {
|
||||
final JSONObject json = new JSONObject(response.body().string());
|
||||
final JSONArray jsonError = json.getJSONArray("error");
|
||||
if (jsonError.length() > 0) {
|
||||
final String errorMsg = jsonError.getString(0);
|
||||
callback.onError(new ExchangeException(response.code(), errorMsg));
|
||||
} else {
|
||||
final JSONObject jsonResult = json.getJSONObject("result");
|
||||
reportSuccess(jsonResult, invert, callback);
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||
}
|
||||
} else {
|
||||
callback.onError(new ExchangeException(response.code(), response.message()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void reportSuccess(JSONObject jsonObject, boolean swapAssets, ExchangeCallback callback) {
|
||||
try {
|
||||
final ExchangeRate exchangeRate = new ExchangeRateImpl(jsonObject, swapAssets);
|
||||
callback.onSuccess(exchangeRate);
|
||||
} catch (JSONException ex) {
|
||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
||||
} catch (ExchangeException ex) {
|
||||
callback.onError(ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.exchange.kraken;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class ExchangeRateImpl implements ExchangeRate {
|
||||
|
||||
private final String baseCurrency;
|
||||
private final String quoteCurrency;
|
||||
private final double rate;
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "kraken.com";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseCurrency() {
|
||||
return baseCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuoteCurrency() {
|
||||
return quoteCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getRate() {
|
||||
return rate;
|
||||
}
|
||||
|
||||
ExchangeRateImpl(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, double rate) {
|
||||
super();
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.quoteCurrency = quoteCurrency;
|
||||
this.rate = rate;
|
||||
}
|
||||
|
||||
ExchangeRateImpl(final JSONObject jsonObject, final boolean swapAssets) throws JSONException, ExchangeException {
|
||||
try {
|
||||
final String key = jsonObject.keys().next(); // we expect only one
|
||||
Pattern pattern = Pattern.compile("^X(.*?)Z(.*?)$");
|
||||
Matcher matcher = pattern.matcher(key);
|
||||
if (matcher.find()) {
|
||||
baseCurrency = swapAssets ? matcher.group(2) : matcher.group(1);
|
||||
quoteCurrency = swapAssets ? matcher.group(1) : matcher.group(2);
|
||||
} else {
|
||||
throw new ExchangeException("no pair returned!");
|
||||
}
|
||||
|
||||
JSONObject pair = jsonObject.getJSONObject(key);
|
||||
JSONArray close = pair.getJSONArray("c");
|
||||
String closePrice = close.getString(0);
|
||||
if (closePrice != null) {
|
||||
try {
|
||||
double rate = Double.parseDouble(closePrice);
|
||||
this.rate = swapAssets ? (1 / rate) : rate;
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new ExchangeException(ex.getLocalizedMessage());
|
||||
}
|
||||
} else {
|
||||
throw new ExchangeException("no close price returned!");
|
||||
}
|
||||
} catch (NoSuchElementException ex) {
|
||||
throw new ExchangeException(ex.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 m2049r@monerujo.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://developer.android.com/training/basics/network-ops/xml
|
||||
|
||||
package com.m2049r.xmrwallet.service.exchange.krakenEcb;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import timber.log.Timber;
|
||||
|
||||
/*
|
||||
Gets the XMR/EUR rate from kraken and then gets the EUR/fiat rate from the ECB
|
||||
*/
|
||||
|
||||
public class ExchangeApiImpl implements ExchangeApi {
|
||||
static public final String BASE_FIAT = "EUR";
|
||||
|
||||
@Override
|
||||
public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
|
||||
@NonNull final ExchangeCallback callback) {
|
||||
Timber.d("B=%s Q=%s", baseCurrency, quoteCurrency);
|
||||
if (baseCurrency.equals(quoteCurrency)) {
|
||||
Timber.d("BASE=QUOTE=1");
|
||||
callback.onSuccess(new ExchangeRateImpl(baseCurrency, quoteCurrency, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Helper.BASE_CRYPTO.equals(baseCurrency)
|
||||
&& !Helper.BASE_CRYPTO.equals(quoteCurrency)) {
|
||||
callback.onError(new IllegalArgumentException("no " + Helper.BASE_CRYPTO + " specified"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String quote = Helper.BASE_CRYPTO.equals(baseCurrency) ? quoteCurrency : baseCurrency;
|
||||
|
||||
final ExchangeApi krakenApi =
|
||||
new com.m2049r.xmrwallet.service.exchange.kraken.ExchangeApiImpl();
|
||||
krakenApi.queryExchangeRate(Helper.BASE_CRYPTO, BASE_FIAT, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate krakenRate) {
|
||||
Timber.d("kraken = %f", krakenRate.getRate());
|
||||
final ExchangeApi ecbApi =
|
||||
new com.m2049r.xmrwallet.service.exchange.ecb.ExchangeApiImpl();
|
||||
ecbApi.queryExchangeRate(BASE_FIAT, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate ecbRate) {
|
||||
Timber.d("ECB = %f", ecbRate.getRate());
|
||||
double rate = ecbRate.getRate() * krakenRate.getRate();
|
||||
Timber.d("Q=%s QC=%s", quote, quoteCurrency);
|
||||
if (!quote.equals(quoteCurrency)) rate = 1.0d / rate;
|
||||
Timber.d("rate = %f", rate);
|
||||
final ExchangeRate exchangeRate =
|
||||
new ExchangeRateImpl(baseCurrency, quoteCurrency, rate);
|
||||
callback.onSuccess(exchangeRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Timber.d(ex);
|
||||
callback.onError(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception ex) {
|
||||
Timber.d(ex);
|
||||
callback.onError(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 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.exchange.krakenEcb;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
|
||||
class ExchangeRateImpl implements ExchangeRate {
|
||||
private final String baseCurrency;
|
||||
private final String quoteCurrency;
|
||||
private final double rate;
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "kraken+ecb";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseCurrency() {
|
||||
return baseCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuoteCurrency() {
|
||||
return quoteCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getRate() {
|
||||
return rate;
|
||||
}
|
||||
|
||||
ExchangeRateImpl(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, double rate) {
|
||||
super();
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.quoteCurrency = quoteCurrency;
|
||||
this.rate = rate;
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public interface NetworkCallback {
|
||||
|
||||
void onSuccess(JSONObject jsonObject);
|
||||
|
||||
void onError(Exception ex);
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public interface ShiftApiCall {
|
||||
|
||||
void call(@NonNull final String path, @NonNull final NetworkCallback callback);
|
||||
|
||||
void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback);
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
|
||||
public interface ShiftCallback<T> {
|
||||
|
||||
void onSuccess(T t);
|
||||
|
||||
void onError(Exception ex);
|
||||
}
|
@ -1,54 +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;
|
||||
|
||||
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;
|
||||
@Getter
|
||||
private final String errorMsg;
|
||||
|
||||
public enum Error {
|
||||
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");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return getErrorMsg();
|
||||
}
|
||||
}
|
@ -1,33 +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;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ShiftException extends Exception {
|
||||
@Getter
|
||||
private final int code;
|
||||
@Getter
|
||||
private final ShiftError error;
|
||||
|
||||
public ShiftException(int code) {
|
||||
this.code = code;
|
||||
this.error = null;
|
||||
}
|
||||
}
|
@ -1,42 +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 CreateOrder {
|
||||
String TAG = "side";
|
||||
|
||||
String getBtcCurrency();
|
||||
|
||||
double getBtcAmount();
|
||||
|
||||
String getBtcAddress();
|
||||
|
||||
String getQuoteId();
|
||||
|
||||
String getOrderId();
|
||||
|
||||
double getXmrAmount();
|
||||
|
||||
String getXmrAddress();
|
||||
|
||||
Date getCreatedAt(); // createdAt
|
||||
|
||||
Date getExpiresAt(); // expiresAt
|
||||
|
||||
}
|
@ -1,27 +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;
|
||||
|
||||
public interface QueryOrderParameters {
|
||||
|
||||
double getLowerLimit();
|
||||
|
||||
double getPrice();
|
||||
|
||||
double getUpperLimit();
|
||||
|
||||
}
|
@ -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,34 +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 RequestQuote {
|
||||
|
||||
double getBtcAmount(); // settleAmount
|
||||
|
||||
String getId(); // id
|
||||
|
||||
Date getCreatedAt(); // createdAt
|
||||
|
||||
Date getExpiresAt(); // expiresAt
|
||||
|
||||
double getXmrAmount(); // depositAmount
|
||||
|
||||
double getPrice(); // rate
|
||||
}
|
@ -1,64 +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 android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||
|
||||
public interface SideShiftApi {
|
||||
int QUERY_INTERVAL = 5000; // ms
|
||||
|
||||
/**
|
||||
* Queries the order parameter.
|
||||
*
|
||||
* @param callback the callback with the OrderParameter object
|
||||
*/
|
||||
void queryOrderParameters(@NonNull final ShiftCallback<QueryOrderParameters> callback);
|
||||
|
||||
/**
|
||||
* Creates an order
|
||||
*
|
||||
* @param xmrAmount the desired XMR amount
|
||||
*/
|
||||
void requestQuote(final double xmrAmount, @NonNull final ShiftCallback<RequestQuote> callback);
|
||||
|
||||
/**
|
||||
* Creates an order
|
||||
*
|
||||
* @param quoteId the desired XMR amount
|
||||
* @param btcAddress the target bitcoin address
|
||||
*/
|
||||
void createOrder(final String quoteId, @NonNull final String btcAddress, @NonNull final ShiftCallback<CreateOrder> callback);
|
||||
|
||||
/**
|
||||
* Queries the order status for given current order
|
||||
*
|
||||
* @param orderId the order ID
|
||||
* @param callback the callback with the OrderStatus object
|
||||
*/
|
||||
void queryOrderStatus(@NonNull final String orderId, @NonNull final ShiftCallback<QueryOrderStatus> callback);
|
||||
|
||||
/*
|
||||
* Returns the URL for manually querying the order status
|
||||
*
|
||||
* @param orderId the order ID
|
||||
*/
|
||||
Uri getQueryOrderUri(String orderId);
|
||||
}
|
@ -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 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.service.shift.sideshift.api.SideShiftApi;
|
||||
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,73 +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.sideshift.api.QueryOrderParameters;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||
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,146 +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.util.DateHelper;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.Getter;
|
||||
import timber.log.Timber;
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -381,196 +381,6 @@ public class Helper {
|
||||
static AlertDialog openDialog = null; // for preventing opening of multiple dialogs
|
||||
static AsyncTask<Void, Void, Boolean> passwordTask = null;
|
||||
|
||||
static public void promptPassword(final Context context, final String wallet, boolean fingerprintDisabled, final PasswordAction action) {
|
||||
if (openDialog != null) return; // we are already asking for password
|
||||
LayoutInflater li = LayoutInflater.from(context);
|
||||
final View promptsView = li.inflate(R.layout.prompt_password, null);
|
||||
|
||||
AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(context);
|
||||
alertDialogBuilder.setView(promptsView);
|
||||
|
||||
final TextInputLayout etPassword = promptsView.findViewById(R.id.etPassword);
|
||||
etPassword.setHint(context.getString(R.string.prompt_password, wallet));
|
||||
|
||||
final TextView tvOpenPrompt = promptsView.findViewById(R.id.tvOpenPrompt);
|
||||
final Drawable icFingerprint = context.getDrawable(R.drawable.ic_fingerprint);
|
||||
final Drawable icError = context.getDrawable(R.drawable.ic_error_red_36dp);
|
||||
final Drawable icInfo = context.getDrawable(R.drawable.ic_info_white_24dp);
|
||||
|
||||
final boolean fingerprintAuthCheck = FingerprintHelper.isFingerPassValid(context, wallet);
|
||||
|
||||
final boolean fingerprintAuthAllowed = !fingerprintDisabled && fingerprintAuthCheck;
|
||||
final CancellationSignal cancelSignal = new CancellationSignal();
|
||||
|
||||
final AtomicBoolean incorrectSavedPass = new AtomicBoolean(false);
|
||||
|
||||
class PasswordTask extends AsyncTask<Void, Void, Boolean> {
|
||||
private String pass;
|
||||
private boolean fingerprintUsed;
|
||||
|
||||
PasswordTask(String pass, boolean fingerprintUsed) {
|
||||
this.pass = pass;
|
||||
this.fingerprintUsed = fingerprintUsed;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icInfo, null, null, null);
|
||||
tvOpenPrompt.setText(context.getText(R.string.prompt_open_wallet));
|
||||
tvOpenPrompt.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... unused) {
|
||||
return processPasswordEntry(context, wallet, pass, fingerprintUsed, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (result) {
|
||||
Helper.hideKeyboardAlways((Activity) context);
|
||||
cancelSignal.cancel();
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
} else {
|
||||
if (fingerprintUsed) {
|
||||
incorrectSavedPass.set(true);
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null);
|
||||
tvOpenPrompt.setText(context.getText(R.string.bad_saved_password));
|
||||
} else {
|
||||
if (!fingerprintAuthAllowed) {
|
||||
tvOpenPrompt.setVisibility(View.GONE);
|
||||
} else if (incorrectSavedPass.get()) {
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null);
|
||||
tvOpenPrompt.setText(context.getText(R.string.bad_password));
|
||||
} else {
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icFingerprint, null, null, null);
|
||||
tvOpenPrompt.setText(context.getText(R.string.prompt_fingerprint_auth));
|
||||
}
|
||||
etPassword.setError(context.getString(R.string.bad_password));
|
||||
}
|
||||
}
|
||||
passwordTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
etPassword.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (etPassword.getError() != null) {
|
||||
etPassword.setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
// set dialog message
|
||||
alertDialogBuilder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(context.getString(R.string.label_ok), null)
|
||||
.setNegativeButton(context.getString(R.string.label_cancel),
|
||||
(dialog, id) -> {
|
||||
action.fail(wallet);
|
||||
Helper.hideKeyboardAlways((Activity) context);
|
||||
cancelSignal.cancel();
|
||||
if (passwordTask != null) {
|
||||
passwordTask.cancel(true);
|
||||
passwordTask = null;
|
||||
}
|
||||
dialog.cancel();
|
||||
openDialog = null;
|
||||
});
|
||||
openDialog = alertDialogBuilder.create();
|
||||
|
||||
final FingerprintManager.AuthenticationCallback fingerprintAuthCallback;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
fingerprintAuthCallback = null;
|
||||
} else {
|
||||
fingerprintAuthCallback = new FingerprintManager.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null);
|
||||
tvOpenPrompt.setText(errString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null);
|
||||
tvOpenPrompt.setText(helpString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
||||
try {
|
||||
String userPass = KeyStoreHelper.loadWalletUserPass(context, wallet);
|
||||
if (passwordTask == null) {
|
||||
passwordTask = new PasswordTask(userPass, true);
|
||||
passwordTask.execute();
|
||||
}
|
||||
} catch (KeyStoreHelper.BrokenPasswordStoreException ex) {
|
||||
etPassword.setError(context.getString(R.string.bad_password));
|
||||
// TODO: better error message here - what would it be?
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icError, null, null, null);
|
||||
tvOpenPrompt.setText(context.getString(R.string.bad_fingerprint));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
openDialog.setOnShowListener(dialog -> {
|
||||
if (fingerprintAuthAllowed && fingerprintAuthCallback != null) {
|
||||
tvOpenPrompt.setCompoundDrawablesRelativeWithIntrinsicBounds(icFingerprint, null, null, null);
|
||||
tvOpenPrompt.setText(context.getText(R.string.prompt_fingerprint_auth));
|
||||
tvOpenPrompt.setVisibility(View.VISIBLE);
|
||||
FingerprintHelper.authenticate(context, cancelSignal, fingerprintAuthCallback);
|
||||
} else {
|
||||
etPassword.requestFocus();
|
||||
}
|
||||
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(view -> {
|
||||
String pass = etPassword.getEditText().getText().toString();
|
||||
if (passwordTask == null) {
|
||||
passwordTask = new PasswordTask(pass, false);
|
||||
passwordTask.execute();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// accept keyboard "ok"
|
||||
etPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
String pass = etPassword.getEditText().getText().toString();
|
||||
if (passwordTask == null) {
|
||||
passwordTask = new PasswordTask(pass, false);
|
||||
passwordTask.execute();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (Helper.preventScreenshot()) {
|
||||
openDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
|
||||
Helper.showKeyboard(openDialog);
|
||||
openDialog.show();
|
||||
}
|
||||
|
||||
public interface PasswordAction {
|
||||
void act(String walletName, String password, boolean fingerprintUsed);
|
||||
|
||||
|
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018 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.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.dialog.HelpFragment;
|
||||
import com.m2049r.xmrwallet.ledger.Ledger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Notice {
|
||||
private static final String PREFS_NAME = "notice";
|
||||
private static List<Notice> notices = null;
|
||||
|
||||
private static final String NOTICE_SHOW_XMRTO_ENABLED_SEND = "notice_xmrto_enabled_send";
|
||||
private static final String NOTICE_SHOW_LEDGER = "notice_ledger_enabled_login";
|
||||
|
||||
private static void init() {
|
||||
synchronized (Notice.class) {
|
||||
if (notices != null) return;
|
||||
notices = new ArrayList<>();
|
||||
if (Helper.ALLOW_SHIFT)
|
||||
notices.add(
|
||||
new Notice(NOTICE_SHOW_XMRTO_ENABLED_SEND,
|
||||
R.string.info_xmrto_enabled,
|
||||
R.string.help_xmrto,
|
||||
1)
|
||||
);
|
||||
if (Ledger.ENABLED)
|
||||
notices.add(
|
||||
new Notice(NOTICE_SHOW_LEDGER,
|
||||
R.string.info_ledger_enabled,
|
||||
R.string.help_create_ledger,
|
||||
1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showAll(ViewGroup parent, String selector) {
|
||||
if (notices == null) init();
|
||||
for (Notice notice : notices) {
|
||||
if (notice.id.matches(selector))
|
||||
notice.show(parent);
|
||||
}
|
||||
}
|
||||
|
||||
private final String id;
|
||||
private final int textResId;
|
||||
private final int helpResId;
|
||||
private final int defaultCount;
|
||||
private transient int count = -1;
|
||||
|
||||
private Notice(final String id, final int textResId, final int helpResId, final int defaultCount) {
|
||||
this.id = id;
|
||||
this.textResId = textResId;
|
||||
this.helpResId = helpResId;
|
||||
this.defaultCount = defaultCount;
|
||||
}
|
||||
|
||||
// show this notice as a child of the given parent view
|
||||
// NB: it assumes the parent is in a Fragment
|
||||
private void show(final ViewGroup parent) {
|
||||
final Context context = parent.getContext();
|
||||
if (getCount(context) <= 0) return; // don't add it
|
||||
|
||||
final LinearLayout ll =
|
||||
(LinearLayout) LayoutInflater.from(context)
|
||||
.inflate(R.layout.template_notice, parent, false);
|
||||
|
||||
((TextView) ll.findViewById(R.id.tvNotice)).setText(textResId);
|
||||
|
||||
final FragmentManager fragmentManager =
|
||||
((FragmentActivity) context).getSupportFragmentManager();
|
||||
ll.setOnClickListener(v -> HelpFragment.display(fragmentManager, helpResId));
|
||||
|
||||
ImageButton ib = ll.findViewById(R.id.ibClose);
|
||||
ib.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ll.setVisibility(View.GONE);
|
||||
decCount(context);
|
||||
}
|
||||
});
|
||||
parent.addView(ll);
|
||||
}
|
||||
|
||||
private int getCount(final Context context) {
|
||||
count = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getInt(id, defaultCount);
|
||||
return count;
|
||||
}
|
||||
|
||||
private void decCount(final Context context) {
|
||||
final SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
if (count < 0) // not initialized yet
|
||||
count = prefs.getInt(id, defaultCount);
|
||||
if (count > 0)
|
||||
prefs.edit().putInt(id, count - 1).apply();
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package com.m2049r.xmrwallet.util;
|
||||
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
@ -17,8 +16,4 @@ public class ServiceHelper {
|
||||
return HttpUrl.parse("https://sideshift.ai/api/v1/");
|
||||
}
|
||||
}
|
||||
|
||||
static public ExchangeApi getExchangeApi() {
|
||||
return new com.m2049r.xmrwallet.service.exchange.krakenEcb.ExchangeApiImpl();
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.
|
||||
*/
|
||||
|
||||
// based on from https://stackoverflow.com/a/45325876 (which did not work for me)
|
||||
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.EditText;
|
||||
|
||||
public class CTextInputLayout extends TextInputLayout {
|
||||
public CTextInputLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CTextInputLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseline() {
|
||||
EditText editText = getEditText();
|
||||
return editText.getBaseline() - (getMeasuredHeight() - editText.getMeasuredHeight());
|
||||
}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.
|
||||
*/
|
||||
|
||||
// based on https://github.com/marcokstephen/StepProgressBar
|
||||
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class DotBar extends View {
|
||||
|
||||
final private int inactiveColor;
|
||||
final private int activeColor;
|
||||
|
||||
final private float dotSize;
|
||||
private float dotSpacing;
|
||||
|
||||
final private int numDots;
|
||||
private int activeDot;
|
||||
|
||||
final private Paint paint;
|
||||
|
||||
public DotBar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DotBar, 0, 0);
|
||||
try {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int desiredWidth = (int) ((numDots * dotSize) + getPaddingLeft() + getPaddingRight());
|
||||
int desiredHeight = (int) (dotSize + getPaddingBottom() + getPaddingTop());
|
||||
|
||||
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
||||
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
int width;
|
||||
int height;
|
||||
|
||||
//Measure Width
|
||||
if (widthMode == MeasureSpec.EXACTLY) {
|
||||
//Must be this size
|
||||
width = widthSize;
|
||||
} else if (widthMode == MeasureSpec.AT_MOST) {
|
||||
//Can't be bigger than...
|
||||
width = Math.min(desiredWidth, widthSize);
|
||||
} else {
|
||||
//Be whatever you want
|
||||
width = desiredWidth;
|
||||
}
|
||||
|
||||
//Measure Height
|
||||
if (heightMode == MeasureSpec.EXACTLY) {
|
||||
//Must be this size
|
||||
height = heightSize;
|
||||
} else if (heightMode == MeasureSpec.AT_MOST) {
|
||||
//Can't be bigger than...
|
||||
height = Math.min(desiredHeight, heightSize);
|
||||
} else {
|
||||
//Be whatever you want
|
||||
height = desiredHeight;
|
||||
}
|
||||
|
||||
dotSpacing = (int) (((1.0 * width - (getPaddingLeft() + getPaddingRight())) / numDots - dotSize) / (numDots - 1));
|
||||
|
||||
Timber.d("dotSpacing=%f", dotSpacing);
|
||||
//MUST CALL THIS
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(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);
|
||||
|
||||
for (int i = 0; i < numDots; i++) {
|
||||
int x = (int) (startingX + i * singleDotSize);
|
||||
if (i == activeDot) {
|
||||
paint.setColor(activeColor);
|
||||
} else {
|
||||
paint.setColor(inactiveColor);
|
||||
}
|
||||
canvas.drawCircle(x + dotSize / 2, startingY + dotSize / 2, dotSize / 2, paint);
|
||||
}
|
||||
}
|
||||
|
||||
public void next() {
|
||||
if (activeDot < numDots - 2) {
|
||||
activeDot++;
|
||||
invalidate();
|
||||
} // else no next - stay stuck at end
|
||||
}
|
||||
|
||||
public void previous() {
|
||||
if (activeDot >= 0) {
|
||||
activeDot--;
|
||||
invalidate();
|
||||
} // else no previous - stay stuck at beginning
|
||||
}
|
||||
|
||||
public void setActiveDot(int i) {
|
||||
if ((i >= 0) && (i < numDots)) {
|
||||
activeDot = i;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public int getActiveDot() {
|
||||
return activeDot;
|
||||
}
|
||||
|
||||
public int getNumDots() {
|
||||
return numDots;
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.
|
||||
*/
|
||||
|
||||
// https://stackoverflow.com/questions/2126717/android-autocompletetextview-show-suggestions-when-no-text-entered
|
||||
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class DropDownEditText extends AppCompatAutoCompleteTextView {
|
||||
|
||||
public DropDownEditText(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public DropDownEditText(Context arg0, AttributeSet arg1) {
|
||||
super(arg0, arg1);
|
||||
}
|
||||
|
||||
public DropDownEditText(Context arg0, AttributeSet arg1, int arg2) {
|
||||
super(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enoughToFilter() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFocusChanged(boolean focused, int direction,
|
||||
Rect previouslyFocusedRect) {
|
||||
super.onFocusChanged(focused, direction, previouslyFocusedRect);
|
||||
if (focused && getAdapter() != null) {
|
||||
performFiltering("", 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,421 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017-2019 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.
|
||||
*/
|
||||
|
||||
// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889
|
||||
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ExchangeEditText extends LinearLayout {
|
||||
|
||||
private double getEnteredAmount() {
|
||||
String enteredAmount = etAmountA.getEditText().getText().toString();
|
||||
try {
|
||||
return Double.parseDouble(enteredAmount);
|
||||
} catch (NumberFormatException ex) {
|
||||
Timber.i(ex.getLocalizedMessage());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean validate(double max, double min) {
|
||||
Timber.d("inProgress=%b", isExchangeInProgress());
|
||||
if (isExchangeInProgress()) {
|
||||
shakeExchangeField();
|
||||
return false;
|
||||
}
|
||||
boolean ok = true;
|
||||
String nativeAmount = getNativeAmount();
|
||||
if (nativeAmount == null) {
|
||||
ok = false;
|
||||
} else {
|
||||
try {
|
||||
double amount = Double.parseDouble(nativeAmount);
|
||||
if ((amount < min) || (amount > max)) {
|
||||
ok = false;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
// this cannot be
|
||||
Timber.e(ex.getLocalizedMessage());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
shakeAmountField();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void shakeAmountField() {
|
||||
etAmountA.startAnimation(Helper.getShakeAnimation(getContext()));
|
||||
}
|
||||
|
||||
void shakeExchangeField() {
|
||||
tvAmountB.startAnimation(Helper.getShakeAnimation(getContext()));
|
||||
}
|
||||
|
||||
public void setAmount(String nativeAmount) {
|
||||
if (nativeAmount != null) {
|
||||
etAmountA.getEditText().setText(nativeAmount);
|
||||
tvAmountB.setText(null);
|
||||
if (sCurrencyA.getSelectedItemPosition() != 0)
|
||||
sCurrencyA.setSelection(0, true); // set native currency & trigger exchange
|
||||
else
|
||||
doExchange();
|
||||
} else {
|
||||
tvAmountB.setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void setEditable(boolean editable) {
|
||||
etAmountA.setEnabled(editable);
|
||||
}
|
||||
|
||||
public String getNativeAmount() {
|
||||
if (isExchangeInProgress()) return null;
|
||||
if (getCurrencyA() == 0)
|
||||
return getCleanAmountString(etAmountA.getEditText().getText().toString());
|
||||
else
|
||||
return getCleanAmountString(tvAmountB.getText().toString());
|
||||
}
|
||||
|
||||
TextInputLayout etAmountA;
|
||||
TextView tvAmountB;
|
||||
Spinner sCurrencyA;
|
||||
Spinner sCurrencyB;
|
||||
ImageView evExchange;
|
||||
ProgressBar pbExchange;
|
||||
|
||||
public int getCurrencyA() {
|
||||
return sCurrencyA.getSelectedItemPosition();
|
||||
}
|
||||
|
||||
public int getCurrencyB() {
|
||||
return sCurrencyB.getSelectedItemPosition();
|
||||
}
|
||||
|
||||
public ExchangeEditText(Context context) {
|
||||
super(context);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public ExchangeEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public ExchangeEditText(Context context,
|
||||
AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the views in the layout.
|
||||
*
|
||||
* @param context the current context for the view.
|
||||
*/
|
||||
void initializeViews(Context context) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.view_exchange_edit, this);
|
||||
}
|
||||
|
||||
void setCurrencyAdapter(Spinner spinner) {
|
||||
List<String> currencies = new ArrayList<>();
|
||||
currencies.add(Helper.BASE_CRYPTO);
|
||||
setCurrencyAdapter(spinner, currencies);
|
||||
}
|
||||
|
||||
protected void setCurrencyAdapter(Spinner spinner, List<String> currencies) {
|
||||
if (Helper.SHOW_EXCHANGERATES)
|
||||
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, currencies);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(spinnerAdapter);
|
||||
}
|
||||
|
||||
void setInitialSpinnerSelections(Spinner baseSpinner, Spinner quoteSpinner) {
|
||||
baseSpinner.setSelection(0, true);
|
||||
quoteSpinner.setSelection(0, true);
|
||||
}
|
||||
|
||||
private boolean isInitialized = false;
|
||||
|
||||
void postInitialize() {
|
||||
setInitialSpinnerSelections(sCurrencyA, sCurrencyB);
|
||||
isInitialized = true;
|
||||
startExchange();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
etAmountA = findViewById(R.id.etAmountA);
|
||||
etAmountA.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
doExchange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
|
||||
});
|
||||
tvAmountB = findViewById(R.id.tvAmountB);
|
||||
sCurrencyA = findViewById(R.id.sCurrencyA);
|
||||
sCurrencyB = findViewById(R.id.sCurrencyB);
|
||||
evExchange = findViewById(R.id.evExchange);
|
||||
pbExchange = findViewById(R.id.pbExchange);
|
||||
|
||||
setCurrencyAdapter(sCurrencyA);
|
||||
setCurrencyAdapter(sCurrencyB);
|
||||
|
||||
post(this::postInitialize);
|
||||
|
||||
// make progress circle gray
|
||||
pbExchange.getIndeterminateDrawable().
|
||||
setColorFilter(ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
|
||||
if (!isInitialized) return;
|
||||
if (position != 0) { // if not native, select native on other
|
||||
sCurrencyB.setSelection(0, true);
|
||||
}
|
||||
doExchange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parentView) {
|
||||
// nothing
|
||||
}
|
||||
});
|
||||
|
||||
sCurrencyB.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(final AdapterView<?> parentView, View selectedItemView, int position, long id) {
|
||||
if (!isInitialized) return;
|
||||
if (position != 0) { // if not native, select native on other
|
||||
sCurrencyA.setSelection(0, true);
|
||||
}
|
||||
doExchange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parentView) {
|
||||
// nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean exchangeRateCacheIsUsable() {
|
||||
return (exchangeRateCache != null) &&
|
||||
((exchangeRateCache.getBaseCurrency().equals(sCurrencyA.getSelectedItem()) &&
|
||||
exchangeRateCache.getQuoteCurrency().equals(sCurrencyB.getSelectedItem())) ||
|
||||
(exchangeRateCache.getBaseCurrency().equals(sCurrencyB.getSelectedItem()) &&
|
||||
exchangeRateCache.getQuoteCurrency().equals(sCurrencyA.getSelectedItem())));
|
||||
}
|
||||
|
||||
private double exchangeRateFromCache() {
|
||||
if (!exchangeRateCacheIsUsable()) return 0;
|
||||
if (exchangeRateCache.getBaseCurrency().equals(sCurrencyA.getSelectedItem())) {
|
||||
return exchangeRateCache.getRate();
|
||||
} else {
|
||||
return 1.0d / exchangeRateCache.getRate();
|
||||
}
|
||||
}
|
||||
|
||||
public void doExchange() {
|
||||
if (!isInitialized) return;
|
||||
tvAmountB.setText(null);
|
||||
if (getCurrencyA() == getCurrencyB()) {
|
||||
exchange(1);
|
||||
return;
|
||||
}
|
||||
// use cached exchange rate if we have it
|
||||
if (!isExchangeInProgress()) {
|
||||
double rate = exchangeRateFromCache();
|
||||
if (rate > 0) {
|
||||
if (prepareExchange()) {
|
||||
exchange(rate);
|
||||
}
|
||||
} else {
|
||||
startExchange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ExchangeApi exchangeApi = ServiceHelper.getExchangeApi();
|
||||
|
||||
// starts exchange through exchange api
|
||||
void startExchange() {
|
||||
String currencyA = (String) sCurrencyA.getSelectedItem();
|
||||
String currencyB = (String) sCurrencyB.getSelectedItem();
|
||||
if ((currencyA == null) || (currencyB == null)) return; // nothing to do
|
||||
execExchange(currencyA, currencyB);
|
||||
}
|
||||
|
||||
void execExchange(String currencyA, String currencyB) {
|
||||
showProgress();
|
||||
queryExchangeRate(currencyA, currencyB,
|
||||
new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
if (isAttachedToWindow())
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
exchange(exchangeRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
Timber.e(e.getLocalizedMessage());
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
exchangeFailed();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void queryExchangeRate(final String base, final String quote, ExchangeCallback callback) {
|
||||
exchangeApi.queryExchangeRate(base, quote, callback);
|
||||
}
|
||||
|
||||
private void exchange(double rate) {
|
||||
double amount = getEnteredAmount();
|
||||
if (rate > 0) {
|
||||
tvAmountB.setText(Helper.getFormattedAmount(rate * amount, getCurrencyB() == 0));
|
||||
} else {
|
||||
tvAmountB.setText("--");
|
||||
Timber.d("No rate!");
|
||||
}
|
||||
}
|
||||
|
||||
private static final String CLEAN_FORMAT = "%." + Helper.XMR_DECIMALS + "f";
|
||||
|
||||
private String getCleanAmountString(String enteredAmount) {
|
||||
try {
|
||||
double amount = Double.parseDouble(enteredAmount);
|
||||
if (amount >= 0) {
|
||||
return String.format(Locale.US, CLEAN_FORMAT, amount);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
boolean prepareExchange() {
|
||||
Timber.d("prepareExchange()");
|
||||
String enteredAmount = etAmountA.getEditText().getText().toString();
|
||||
if (!enteredAmount.isEmpty()) {
|
||||
String cleanAmount = getCleanAmountString(enteredAmount);
|
||||
Timber.d("cleanAmount = %s", cleanAmount);
|
||||
if (cleanAmount == null) {
|
||||
shakeAmountField();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void exchangeFailed() {
|
||||
hideProgress();
|
||||
exchange(0);
|
||||
}
|
||||
|
||||
// cache for exchange rate
|
||||
ExchangeRate exchangeRateCache = null;
|
||||
|
||||
public void exchange(ExchangeRate exchangeRate) {
|
||||
hideProgress();
|
||||
// make sure this is what we want
|
||||
if (!exchangeRate.getBaseCurrency().equals(sCurrencyA.getSelectedItem()) ||
|
||||
!exchangeRate.getQuoteCurrency().equals(sCurrencyB.getSelectedItem())) {
|
||||
// something's wrong
|
||||
Timber.i("Currencies don't match! A: %s==%s B: %s==%s",
|
||||
exchangeRate.getBaseCurrency(), sCurrencyA.getSelectedItem(),
|
||||
exchangeRate.getQuoteCurrency(), sCurrencyB.getSelectedItem());
|
||||
return;
|
||||
}
|
||||
|
||||
exchangeRateCache = exchangeRate;
|
||||
if (prepareExchange()) {
|
||||
exchange(exchangeRate.getRate());
|
||||
}
|
||||
}
|
||||
|
||||
void showProgress() {
|
||||
pbExchange.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private boolean isExchangeInProgress() {
|
||||
return pbExchange.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
private void hideProgress() {
|
||||
pbExchange.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017-2019 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.
|
||||
*/
|
||||
|
||||
// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889
|
||||
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ExchangeOtherEditText extends ExchangeEditText {
|
||||
/*
|
||||
all exchanges are done through XMR
|
||||
baseCurrency is the native currency
|
||||
*/
|
||||
|
||||
String baseCurrency = null; // not XMR
|
||||
private double exchangeRate = 0; // baseCurrency to XMR
|
||||
|
||||
public void setExchangeRate(double rate) {
|
||||
exchangeRate = rate;
|
||||
post(this::startExchange);
|
||||
}
|
||||
|
||||
public void setBaseCurrency(@NonNull String symbol) {
|
||||
if (symbol.equals(baseCurrency)) return;
|
||||
baseCurrency = symbol;
|
||||
setCurrencyAdapter(sCurrencyA);
|
||||
setCurrencyAdapter(sCurrencyB);
|
||||
post(this::postInitialize);
|
||||
}
|
||||
|
||||
private void setBaseCurrency(Context context, AttributeSet attrs) {
|
||||
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExchangeEditText, 0, 0);
|
||||
try {
|
||||
baseCurrency = ta.getString(R.styleable.ExchangeEditText_baseSymbol);
|
||||
if (baseCurrency == null)
|
||||
throw new IllegalArgumentException("base currency must be set");
|
||||
} finally {
|
||||
ta.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public ExchangeOtherEditText(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setBaseCurrency(context, attrs);
|
||||
}
|
||||
|
||||
public ExchangeOtherEditText(Context context,
|
||||
AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
setBaseCurrency(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
void setCurrencyAdapter(Spinner spinner) {
|
||||
List<String> currencies = new ArrayList<>();
|
||||
if (!baseCurrency.equals(Helper.BASE_CRYPTO)) currencies.add(baseCurrency);
|
||||
currencies.add(Helper.BASE_CRYPTO);
|
||||
setCurrencyAdapter(spinner, currencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
void setInitialSpinnerSelections(Spinner baseSpinner, Spinner quoteSpinner) {
|
||||
baseSpinner.setSelection(0, true);
|
||||
quoteSpinner.setSelection(1, true);
|
||||
}
|
||||
|
||||
private void localExchange(final String base, final String quote, final double rate) {
|
||||
exchange(new ExchangeRate() {
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "Local";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseCurrency() {
|
||||
return base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuoteCurrency() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getRate() {
|
||||
return rate;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
void execExchange(String currencyA, String currencyB) {
|
||||
if (!currencyA.equals(baseCurrency) && !currencyB.equals(baseCurrency)) {
|
||||
throw new IllegalStateException("I can only exchange " + baseCurrency);
|
||||
}
|
||||
|
||||
showProgress();
|
||||
|
||||
Timber.d("execExchange(%s, %s)", currencyA, currencyB);
|
||||
|
||||
// first deal with XMR/baseCurrency & baseCurrency/XMR
|
||||
|
||||
if (currencyA.equals(Helper.BASE_CRYPTO) && (currencyB.equals(baseCurrency))) {
|
||||
localExchange(currencyA, currencyB, 1.0d / exchangeRate);
|
||||
return;
|
||||
}
|
||||
if (currencyA.equals(baseCurrency) && (currencyB.equals(Helper.BASE_CRYPTO))) {
|
||||
localExchange(currencyA, currencyB, exchangeRate);
|
||||
return;
|
||||
}
|
||||
|
||||
// next, deal with XMR/baseCurrency
|
||||
|
||||
if (currencyA.equals(baseCurrency)) {
|
||||
queryExchangeRate(Helper.BASE_CRYPTO, currencyB, exchangeRate, true);
|
||||
} else {
|
||||
queryExchangeRate(currencyA, Helper.BASE_CRYPTO, 1.0d / exchangeRate, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void queryExchangeRate(final String base, final String quote, final double factor,
|
||||
final boolean baseIsBaseCrypto) {
|
||||
queryExchangeRate(base, quote,
|
||||
new ExchangeCallback() {
|
||||
@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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseCurrency() {
|
||||
return baseIsBaseCrypto ? baseCurrency : base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuoteCurrency() {
|
||||
return baseIsBaseCrypto ? quote : baseCurrency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getRate() {
|
||||
return exchangeRate.getRate() * factor;
|
||||
}
|
||||
};
|
||||
exchange(xchange);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
Timber.e(e.getLocalizedMessage());
|
||||
new Handler(Looper.getMainLooper()).post(() -> exchangeFailed());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,469 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.
|
||||
*/
|
||||
|
||||
// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889
|
||||
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ExchangeView extends LinearLayout {
|
||||
String xmrAmount = null;
|
||||
String notXmrAmount = null;
|
||||
|
||||
public void enable(boolean enable) {
|
||||
etAmount.setEnabled(enable);
|
||||
sCurrencyA.setEnabled(enable);
|
||||
sCurrencyB.setEnabled(enable);
|
||||
}
|
||||
|
||||
void setXmr(String xmr) {
|
||||
xmrAmount = xmr;
|
||||
if (onNewAmountListener != null) {
|
||||
onNewAmountListener.onNewAmount(xmr);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAmount(String xmrAmount) {
|
||||
if (xmrAmount != null) {
|
||||
setCurrencyA(0);
|
||||
etAmount.getEditText().setText(xmrAmount);
|
||||
setXmr(xmrAmount);
|
||||
this.notXmrAmount = null;
|
||||
doExchange();
|
||||
} else {
|
||||
setXmr(null);
|
||||
this.notXmrAmount = null;
|
||||
tvAmountB.setText("--");
|
||||
}
|
||||
}
|
||||
|
||||
public String getAmount() {
|
||||
return xmrAmount;
|
||||
}
|
||||
|
||||
public void setError(String msg) {
|
||||
etAmount.setError(msg);
|
||||
}
|
||||
|
||||
TextInputLayout etAmount;
|
||||
TextView tvAmountB;
|
||||
Spinner sCurrencyA;
|
||||
Spinner sCurrencyB;
|
||||
ImageView evExchange;
|
||||
ProgressBar pbExchange;
|
||||
|
||||
|
||||
public void setCurrencyA(int currency) {
|
||||
if ((currency != 0) && (getCurrencyB() != 0)) {
|
||||
setCurrencyB(0);
|
||||
}
|
||||
sCurrencyA.setSelection(currency, true);
|
||||
doExchange();
|
||||
}
|
||||
|
||||
public void setCurrencyB(int currency) {
|
||||
if ((currency != 0) && (getCurrencyA() != 0)) {
|
||||
setCurrencyA(0);
|
||||
}
|
||||
sCurrencyB.setSelection(currency, true);
|
||||
doExchange();
|
||||
}
|
||||
|
||||
public int getCurrencyA() {
|
||||
return sCurrencyA.getSelectedItemPosition();
|
||||
}
|
||||
|
||||
public int getCurrencyB() {
|
||||
return sCurrencyB.getSelectedItemPosition();
|
||||
}
|
||||
|
||||
public ExchangeView(Context context) {
|
||||
super(context);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public ExchangeView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public ExchangeView(Context context,
|
||||
AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the views in the layout.
|
||||
*
|
||||
* @param context the current context for the view.
|
||||
*/
|
||||
private void initializeViews(Context context) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.view_exchange, this);
|
||||
}
|
||||
|
||||
void setCurrencyAdapter(Spinner spinner) {
|
||||
List<String> currencies = new ArrayList<>();
|
||||
currencies.add(Helper.BASE_CRYPTO);
|
||||
if (Helper.SHOW_EXCHANGERATES)
|
||||
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, currencies);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(spinnerAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
etAmount = findViewById(R.id.etAmount);
|
||||
tvAmountB = findViewById(R.id.tvAmountB);
|
||||
sCurrencyA = findViewById(R.id.sCurrencyA);
|
||||
sCurrencyB = findViewById(R.id.sCurrencyB);
|
||||
evExchange = findViewById(R.id.evExchange);
|
||||
pbExchange = findViewById(R.id.pbExchange);
|
||||
|
||||
setCurrencyAdapter(sCurrencyA);
|
||||
setCurrencyAdapter(sCurrencyB);
|
||||
|
||||
// make progress circle gray
|
||||
pbExchange.getIndeterminateDrawable().
|
||||
setColorFilter(ThemeHelper.getThemedColor(getContext(), R.attr.colorPrimaryVariant),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
|
||||
if (position != 0) { // if not XMR, select XMR on other
|
||||
sCurrencyB.setSelection(0, true);
|
||||
}
|
||||
doExchange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parentView) {
|
||||
// nothing (yet?)
|
||||
}
|
||||
});
|
||||
|
||||
sCurrencyB.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(final AdapterView<?> parentView, View selectedItemView, int position, long id) {
|
||||
if (position != 0) { // if not XMR, select XMR on other
|
||||
sCurrencyA.setSelection(0, true);
|
||||
}
|
||||
doExchange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parentView) {
|
||||
// nothing
|
||||
}
|
||||
});
|
||||
|
||||
etAmount.getEditText().setOnFocusChangeListener(new OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (!hasFocus) {
|
||||
doExchange();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
etAmount.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
doExchange();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
etAmount.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
etAmount.setError(null);
|
||||
clearAmounts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
final static double MAX_AMOUNT_XMR = 1000;
|
||||
final static double MAX_AMOUNT_NOTXMR = 100000;
|
||||
|
||||
public boolean checkEnteredAmount() {
|
||||
boolean ok = true;
|
||||
Timber.d("checkEnteredAmount");
|
||||
String amountEntry = etAmount.getEditText().getText().toString();
|
||||
if (!amountEntry.isEmpty()) {
|
||||
try {
|
||||
double a = Double.parseDouble(amountEntry);
|
||||
double maxAmount = (getCurrencyA() == 0) ? MAX_AMOUNT_XMR : MAX_AMOUNT_NOTXMR;
|
||||
if (a > (maxAmount)) {
|
||||
etAmount.setError(getResources().
|
||||
getString(R.string.receive_amount_too_big,
|
||||
String.format(Locale.US, "%,.0f", maxAmount)));
|
||||
ok = false;
|
||||
} else if (a < 0) {
|
||||
etAmount.setError(getResources().getString(R.string.receive_amount_negative));
|
||||
ok = false;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
etAmount.setError(getResources().getString(R.string.receive_amount_nan));
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
etAmount.setError(null);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
public void doExchange() {
|
||||
tvAmountB.setText("--");
|
||||
// use cached exchange rate if we have it
|
||||
if (!isExchangeInProgress()) {
|
||||
String enteredCurrencyA = (String) sCurrencyA.getSelectedItem();
|
||||
String enteredCurrencyB = (String) sCurrencyB.getSelectedItem();
|
||||
if ((enteredCurrencyA + enteredCurrencyB).equals(assetPair)) {
|
||||
if (prepareExchange()) {
|
||||
exchange(assetRate);
|
||||
} else {
|
||||
clearAmounts();
|
||||
}
|
||||
} else {
|
||||
clearAmounts();
|
||||
startExchange();
|
||||
}
|
||||
} else {
|
||||
clearAmounts();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAmounts() {
|
||||
if ((xmrAmount != null) || (notXmrAmount != null)) {
|
||||
tvAmountB.setText("--");
|
||||
setXmr(null);
|
||||
notXmrAmount = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final ExchangeApi exchangeApi = ServiceHelper.getExchangeApi();
|
||||
|
||||
void startExchange() {
|
||||
showProgress();
|
||||
String currencyA = (String) sCurrencyA.getSelectedItem();
|
||||
String currencyB = (String) sCurrencyB.getSelectedItem();
|
||||
|
||||
exchangeApi.queryExchangeRate(currencyA, currencyB,
|
||||
new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
if (isAttachedToWindow())
|
||||
new Handler(Looper.getMainLooper()).post(() -> exchange(exchangeRate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
Timber.e(e.getLocalizedMessage());
|
||||
new Handler(Looper.getMainLooper()).post(() -> exchangeFailed());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void exchange(double rate) {
|
||||
if (getCurrencyA() == 0) {
|
||||
if (xmrAmount == null) return;
|
||||
if (!xmrAmount.isEmpty() && (rate > 0)) {
|
||||
double amountB = rate * Double.parseDouble(xmrAmount);
|
||||
notXmrAmount = Helper.getFormattedAmount(amountB, getCurrencyB() == 0);
|
||||
} else {
|
||||
notXmrAmount = "";
|
||||
}
|
||||
tvAmountB.setText(notXmrAmount);
|
||||
} else if (getCurrencyB() == 0) {
|
||||
if (notXmrAmount == null) return;
|
||||
if (!notXmrAmount.isEmpty() && (rate > 0)) {
|
||||
double amountB = rate * Double.parseDouble(notXmrAmount);
|
||||
setXmr(Helper.getFormattedAmount(amountB, true));
|
||||
} else {
|
||||
setXmr("");
|
||||
}
|
||||
tvAmountB.setText(xmrAmount);
|
||||
} else { // no XMR currency - cannot happen!
|
||||
throw new IllegalStateException("No XMR currency!");
|
||||
}
|
||||
if (rate == 0)
|
||||
tvAmountB.setText("--");
|
||||
}
|
||||
|
||||
boolean prepareExchange() {
|
||||
Timber.d("prepareExchange()");
|
||||
if (checkEnteredAmount()) {
|
||||
String enteredAmount = etAmount.getEditText().getText().toString();
|
||||
if (!enteredAmount.isEmpty()) {
|
||||
String cleanAmount = "";
|
||||
if (getCurrencyA() == 0) {
|
||||
// sanitize the input
|
||||
cleanAmount = Helper.getDisplayAmount(Wallet.getAmountFromString(enteredAmount));
|
||||
setXmr(cleanAmount);
|
||||
notXmrAmount = null;
|
||||
Timber.d("cleanAmount = %s", cleanAmount);
|
||||
} else if (getCurrencyB() == 0) { // we use B & 0 here for the else below ...
|
||||
// sanitize the input
|
||||
double amountA = Double.parseDouble(enteredAmount);
|
||||
cleanAmount = String.format(Locale.US, "%.2f", amountA);
|
||||
setXmr(null);
|
||||
notXmrAmount = cleanAmount;
|
||||
} else { // no XMR currency - cannot happen!
|
||||
Timber.e("No XMR currency!");
|
||||
setXmr(null);
|
||||
notXmrAmount = null;
|
||||
return false;
|
||||
}
|
||||
Timber.d("prepareExchange() %s", cleanAmount);
|
||||
} else {
|
||||
setXmr("");
|
||||
notXmrAmount = "";
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
setXmr(null);
|
||||
notXmrAmount = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void exchangeFailed() {
|
||||
hideProgress();
|
||||
exchange(0);
|
||||
if (onFailedExchangeListener != null) {
|
||||
onFailedExchangeListener.onFailedExchange();
|
||||
}
|
||||
}
|
||||
|
||||
String assetPair = null;
|
||||
double assetRate = 0;
|
||||
|
||||
public void exchange(ExchangeRate exchangeRate) {
|
||||
hideProgress();
|
||||
// first, make sure this is what we want
|
||||
String enteredCurrencyA = (String) sCurrencyA.getSelectedItem();
|
||||
String enteredCurrencyB = (String) sCurrencyB.getSelectedItem();
|
||||
if (!exchangeRate.getBaseCurrency().equals(enteredCurrencyA)
|
||||
|| !exchangeRate.getQuoteCurrency().equals(enteredCurrencyB)) {
|
||||
// something's wrong
|
||||
Timber.e("Currencies don't match!");
|
||||
return;
|
||||
}
|
||||
assetPair = enteredCurrencyA + enteredCurrencyB;
|
||||
assetRate = exchangeRate.getRate();
|
||||
if (prepareExchange()) {
|
||||
exchange(exchangeRate.getRate());
|
||||
}
|
||||
}
|
||||
|
||||
private void showProgress() {
|
||||
pbExchange.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private boolean isExchangeInProgress() {
|
||||
return pbExchange.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
private void hideProgress() {
|
||||
pbExchange.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// Hooks
|
||||
public interface OnNewAmountListener {
|
||||
void onNewAmount(String xmr);
|
||||
}
|
||||
|
||||
OnNewAmountListener onNewAmountListener;
|
||||
|
||||
public void setOnNewAmountListener(OnNewAmountListener listener) {
|
||||
onNewAmountListener = listener;
|
||||
}
|
||||
|
||||
public interface OnAmountInvalidatedListener {
|
||||
void onAmountInvalidated();
|
||||
}
|
||||
|
||||
OnAmountInvalidatedListener onAmountInvalidatedListener;
|
||||
|
||||
public void setOnAmountInvalidatedListener(OnAmountInvalidatedListener listener) {
|
||||
onAmountInvalidatedListener = listener;
|
||||
}
|
||||
|
||||
public interface OnFailedExchangeListener {
|
||||
void onFailedExchange();
|
||||
}
|
||||
|
||||
OnFailedExchangeListener onFailedExchangeListener;
|
||||
|
||||
public void setOnFailedExchangeListener(OnFailedExchangeListener listener) {
|
||||
onFailedExchangeListener = listener;
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
public class PasswordEntryView extends TextInputLayout implements TextWatcher {
|
||||
final private Zxcvbn zxcvbn = new Zxcvbn();
|
||||
|
||||
public PasswordEntryView(@NonNull Context context) {
|
||||
super(context, null);
|
||||
}
|
||||
|
||||
public PasswordEntryView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs, R.attr.textInputStyle);
|
||||
}
|
||||
|
||||
public PasswordEntryView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(@NonNull View child, int index, @NonNull final ViewGroup.LayoutParams params) {
|
||||
super.addView(child, index, params);
|
||||
final EditText et = getEditText();
|
||||
if (et != null)
|
||||
et.addTextChangedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String password = s.toString();
|
||||
int icon = 0;
|
||||
if (!password.isEmpty()) {
|
||||
final double strength = Math.min(zxcvbn.measure(password).getGuessesLog10(), 15) / 3 * 20; // 0-100%
|
||||
if (strength < 21)
|
||||
icon = R.drawable.ic_smiley_sad_filled;
|
||||
else if (strength < 40)
|
||||
icon = R.drawable.ic_smiley_meh_filled;
|
||||
else if (strength < 60)
|
||||
icon = R.drawable.ic_smiley_neutral_filled;
|
||||
else if (strength < 80)
|
||||
icon = R.drawable.ic_smiley_happy_filled;
|
||||
else if (strength < 99)
|
||||
icon = R.drawable.ic_smiley_ecstatic_filled;
|
||||
else
|
||||
icon = R.drawable.ic_smiley_gunther_filled;
|
||||
}
|
||||
setErrorIconDrawable(icon);
|
||||
if (icon != 0)
|
||||
setError(" ");
|
||||
else setError(null);
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
public class SendProgressView extends LinearLayout {
|
||||
|
||||
public SendProgressView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public SendProgressView(Context context,
|
||||
AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
private void initializeViews(Context context) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.view_send_progress, this);
|
||||
}
|
||||
|
||||
|
||||
View pbProgress;
|
||||
View llMessage;
|
||||
TextView tvCode;
|
||||
TextView tvMessage;
|
||||
TextView tvSolution;
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
pbProgress = findViewById(R.id.pbProgress);
|
||||
llMessage = findViewById(R.id.llMessage);
|
||||
tvCode = findViewById(R.id.tvCode);
|
||||
tvMessage = findViewById(R.id.tvMessage);
|
||||
tvSolution = findViewById(R.id.tvSolution);
|
||||
}
|
||||
|
||||
public void showProgress(String progressText) {
|
||||
pbProgress.setVisibility(VISIBLE);
|
||||
tvCode.setVisibility(INVISIBLE);
|
||||
tvMessage.setText(progressText);
|
||||
llMessage.setVisibility(VISIBLE);
|
||||
tvSolution.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
public void hideProgress() {
|
||||
pbProgress.setVisibility(INVISIBLE);
|
||||
llMessage.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
public void showMessage(String code, String message, String solution) {
|
||||
tvCode.setText(code);
|
||||
tvMessage.setText(message);
|
||||
tvSolution.setText(solution);
|
||||
tvCode.setVisibility(VISIBLE);
|
||||
llMessage.setVisibility(VISIBLE);
|
||||
tvSolution.setVisibility(VISIBLE);
|
||||
pbProgress.setVisibility(INVISIBLE);
|
||||
}
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 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.
|
||||
*/
|
||||
|
||||
// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889
|
||||
|
||||
package com.m2049r.xmrwallet.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Toolbar extends MaterialToolbar {
|
||||
public interface OnButtonListener {
|
||||
void onButton(int type);
|
||||
}
|
||||
|
||||
OnButtonListener onButtonListener;
|
||||
|
||||
public void setOnButtonListener(OnButtonListener listener) {
|
||||
onButtonListener = listener;
|
||||
}
|
||||
|
||||
ImageView toolbarImage;
|
||||
TextView toolbarTitle;
|
||||
TextView toolbarSubtitle;
|
||||
ImageButton bSettings;
|
||||
|
||||
public Toolbar(Context context) {
|
||||
super(context);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public Toolbar(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public Toolbar(Context context,
|
||||
AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the views in the layout.
|
||||
*
|
||||
* @param context the current context for the view.
|
||||
*/
|
||||
private void initializeViews(Context context) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.view_toolbar, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
toolbarImage = findViewById(R.id.toolbarImage);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// the vector image does not work well for androis < Nougat
|
||||
toolbarImage.getLayoutParams().width = (int) getResources().getDimension(R.dimen.logo_width);
|
||||
toolbarImage.setImageResource(R.drawable.logo_horizontol_xmrujo);
|
||||
}
|
||||
|
||||
toolbarTitle = findViewById(R.id.toolbarTitle);
|
||||
toolbarSubtitle = findViewById(R.id.toolbarSubtitle);
|
||||
bSettings = findViewById(R.id.bSettings);
|
||||
bSettings.setOnClickListener(v -> {
|
||||
if (onButtonListener != null) {
|
||||
onButtonListener.onButton(buttonType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setTitle(String title, String subtitle) {
|
||||
setTitle(title);
|
||||
setSubtitle(subtitle);
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
toolbarTitle.setText(title);
|
||||
if (title != null) {
|
||||
toolbarImage.setVisibility(View.INVISIBLE);
|
||||
toolbarTitle.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
toolbarImage.setVisibility(View.VISIBLE);
|
||||
toolbarTitle.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public final static int BUTTON_NONE = 0;
|
||||
public final static int BUTTON_BACK = 1;
|
||||
public final static int BUTTON_CLOSE = 2;
|
||||
public final static int BUTTON_SETTINGS = 3;
|
||||
public final static int BUTTON_CANCEL = 4;
|
||||
|
||||
int buttonType = BUTTON_SETTINGS;
|
||||
|
||||
public void setButton(int type) {
|
||||
switch (type) {
|
||||
case BUTTON_BACK:
|
||||
Timber.d("BUTTON_BACK");
|
||||
bSettings.setImageResource(R.drawable.ic_arrow_back);
|
||||
bSettings.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case BUTTON_CLOSE:
|
||||
Timber.d("BUTTON_CLOSE");
|
||||
bSettings.setImageResource(R.drawable.ic_close_white_24dp);
|
||||
bSettings.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case BUTTON_SETTINGS:
|
||||
Timber.d("BUTTON_SETTINGS");
|
||||
bSettings.setImageResource(R.drawable.ic_settings);
|
||||
bSettings.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case BUTTON_CANCEL:
|
||||
Timber.d("BUTTON_CANCEL");
|
||||
bSettings.setImageResource(R.drawable.ic_close_white_24dp);
|
||||
bSettings.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
case BUTTON_NONE:
|
||||
default:
|
||||
Timber.d("BUTTON_NONE");
|
||||
bSettings.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
buttonType = type;
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
toolbarSubtitle.setText(subtitle);
|
||||
if (subtitle != null) {
|
||||
toolbarSubtitle.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
toolbarSubtitle.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user