From 93025c5e1bea1106f0ec4f9cb7a7204eb4efcdc4 Mon Sep 17 00:00:00 2001 From: m2049r <30435443+m2049r@users.noreply.github.com> Date: Sat, 2 Sep 2017 00:00:54 +0200 Subject: [PATCH 1/3] Receive QR code --- .../m2049r/xmrwallet/GenerateFragment.java | 13 +- .../xmrwallet/GenerateReviewFragment.java | 6 +- .../com/m2049r/xmrwallet/LoginActivity.java | 60 ++-- .../com/m2049r/xmrwallet/LoginFragment.java | 24 +- .../com/m2049r/xmrwallet/ReceiveFragment.java | 265 ++++++++++++++++++ .../com/m2049r/xmrwallet/SendFragment.java | 13 +- .../com/m2049r/xmrwallet/WalletActivity.java | 10 - .../m2049r/xmrwallet/model/WalletManager.java | 69 +++-- .../xmrwallet/service/WalletService.java | 4 +- .../com/m2049r/xmrwallet/util/Helper.java | 37 ++- app/src/main/res/drawable/ic_monero_32dp.xml | 20 ++ app/src/main/res/drawable/ic_monero_qr.xml | 20 ++ .../drawable/ic_notification_sync_32_32.png | Bin 1738 -> 0 bytes app/src/main/res/drawable/xmr_logo_256.png | Bin 8998 -> 0 bytes app/src/main/res/layout/receive_fragment.xml | 107 +++++++ app/src/main/res/menu/list_menu.xml | 4 + app/src/main/res/values/strings.xml | 5 + 17 files changed, 574 insertions(+), 83 deletions(-) create mode 100644 app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java create mode 100644 app/src/main/res/drawable/ic_monero_32dp.xml create mode 100644 app/src/main/res/drawable/ic_monero_qr.xml delete mode 100644 app/src/main/res/drawable/ic_notification_sync_32_32.png delete mode 100644 app/src/main/res/drawable/xmr_logo_256.png create mode 100644 app/src/main/res/layout/receive_fragment.xml diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index 53610b2..8fa139e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -69,12 +69,9 @@ public class GenerateFragment extends Fragment { bGenerate = (Button) view.findViewById(R.id.bGenerate); etWalletMnemonic.setRawInputType(InputType.TYPE_CLASS_TEXT); - etWalletAddress.setRawInputType(InputType.TYPE_CLASS_TEXT); - etWalletViewKey.setRawInputType(InputType.TYPE_CLASS_TEXT); - etWalletSpendKey.setRawInputType(InputType.TYPE_CLASS_TEXT); - - boolean testnet = WalletManager.getInstance().isTestNet(); - //etWalletMnemonic.setTextIsSelectable(testnet); + etWalletAddress.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etWalletViewKey.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etWalletSpendKey.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); Helper.showKeyboard(getActivity()); etWalletName.addTextChangedListener(new TextWatcher() { @@ -298,8 +295,8 @@ public class GenerateFragment extends Fragment { private void generateWallet() { String name = etWalletName.getText().toString(); if (name.length() == 0) return; - String walletPath = Helper.getWalletPath(getActivity(), name); - if (WalletManager.getInstance().walletExists(walletPath)) { + File walletFile = Helper.getWalletFile(getActivity(), name); + if (WalletManager.getInstance().walletExists(walletFile)) { Toast.makeText(getActivity(), getString(R.string.generate_wallet_exists), Toast.LENGTH_LONG).show(); etWalletName.requestFocus(); return; diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index ece553c..f2624bc 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -79,10 +79,10 @@ public class GenerateReviewFragment extends Fragment { Bundle b = getArguments(); String type = b.getString("type"); if (!type.equals(VIEW_WALLET)) { - String name = b.getString("name"); + String path = b.getString("path"); String password = b.getString("password"); - tvWalletName.setText(new File(name).getName()); - show(name, password, type); + tvWalletName.setText(new File(path).getName()); + show(path, password, type); } else { show(walletCallback.getWallet(), null, type); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index fc64961..d96ae2b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -85,8 +85,8 @@ public class LoginActivity extends AppCompatActivity } Log.d(TAG, "selected wallet is ." + walletName + "."); // now it's getting real, check if wallet exists - String walletPath = Helper.getWalletPath(this, walletName); - if (WalletManager.getInstance().walletExists(walletPath)) { + File walletFile = Helper.getWalletFile(this, walletName); + if (WalletManager.getInstance().walletExists(walletFile)) { promptPassword(walletName, new PasswordAction() { @Override public void action(String walletName, String password) { @@ -101,12 +101,12 @@ public class LoginActivity extends AppCompatActivity @Override public void onWalletDetails(final String walletName) { Log.d(TAG, "details for wallet ." + walletName + "."); - final String walletPath = Helper.getWalletPath(this, walletName); - if (WalletManager.getInstance().walletExists(walletPath)) { + final File walletFile = Helper.getWalletFile(this, walletName); + if (WalletManager.getInstance().walletExists(walletFile)) { promptPassword(walletName, new PasswordAction() { @Override public void action(String walletName, String password) { - startDetails(walletPath, password, GenerateReviewFragment.VIEW_DETAILS); + startDetails(walletFile, password, GenerateReviewFragment.VIEW_DETAILS); } }); } else { // this cannot really happen as we prefilter choices @@ -114,6 +114,18 @@ public class LoginActivity extends AppCompatActivity } } + @Override + public void onWalletReceive(final String walletName) { + Log.d(TAG, "receive for wallet ." + walletName + "."); + final File walletFile = Helper.getWalletFile(this, walletName); + if (WalletManager.getInstance().walletExists(walletFile)) { + String address = WalletManager.getInstance().getWalletInfo(walletFile).address; + startReceive(address); + } else { // this cannot really happen as we prefilter choices + Toast.makeText(this, getString(R.string.bad_wallet), Toast.LENGTH_SHORT).show(); + } + } + @Override public void onAddWallet() { startGenerateFragment(); @@ -251,15 +263,22 @@ public class LoginActivity extends AppCompatActivity startActivity(intent); } - void startDetails(final String walletPath, final String password, String type) { + void startDetails(final File walletFile, final String password, String type) { Log.d(TAG, "startDetails()"); Bundle b = new Bundle(); - b.putString("name", walletPath); + b.putString("path", walletFile.getAbsolutePath()); b.putString("password", password); b.putString("type", type); startReviewFragment(b); } + void startReceive(String address) { + Log.d(TAG, "startReceive()"); + Bundle b = new Bundle(); + b.putString("address", address); + startReceiveFragment(b); + } + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { Log.d(TAG, "onRequestPermissionsResult()"); @@ -297,6 +316,11 @@ public class LoginActivity extends AppCompatActivity Log.d(TAG, "GenerateReviewFragment placed"); } + void startReceiveFragment(Bundle extras) { + replaceFragment(new ReceiveFragment(), null, extras); + Log.d(TAG, "ReceiveFragment placed"); + } + void replaceFragment(Fragment newFragment, String stackName, Bundle extras) { if (extras != null) { newFragment.setArguments(extras); @@ -345,29 +369,29 @@ public class LoginActivity extends AppCompatActivity return; } - String newWalletPath = new File(newWalletFolder, name).getAbsolutePath(); - boolean success = walletCreator.createWallet(newWalletPath, password); + File newWalletFile = new File(newWalletFolder, name); + boolean success = walletCreator.createWallet(newWalletFile, password); if (success) { - startDetails(newWalletPath, password, GenerateReviewFragment.VIEW_ACCEPT); + startDetails(newWalletFile, password, GenerateReviewFragment.VIEW_ACCEPT); } else { Toast.makeText(LoginActivity.this, getString(R.string.generate_wallet_create_failed), Toast.LENGTH_LONG).show(); - Log.e(TAG, "Could not create new wallet in " + newWalletPath); + Log.e(TAG, "Could not create new wallet in " + newWalletFile.getAbsolutePath()); } } interface WalletCreator { - boolean createWallet(String path, String password); + boolean createWallet(File aFile, String password); } @Override public void onGenerate(String name, String password) { createWallet(name, password, new WalletCreator() { - public boolean createWallet(String path, String password) { + public boolean createWallet(File aFile, String password) { Wallet newWallet = WalletManager.getInstance() - .createWallet(path, password, MNEMONIC_LANGUAGE); + .createWallet(aFile, password, MNEMONIC_LANGUAGE); boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); if (!success) Log.e(TAG, newWallet.getErrorString()); newWallet.close(); @@ -380,8 +404,8 @@ public class LoginActivity extends AppCompatActivity public void onGenerate(String name, String password, final String seed, final long restoreHeight) { createWallet(name, password, new WalletCreator() { - public boolean createWallet(String path, String password) { - Wallet newWallet = WalletManager.getInstance().recoveryWallet(path, seed, restoreHeight); + public boolean createWallet(File aFile, String password) { + Wallet newWallet = WalletManager.getInstance().recoveryWallet(aFile, seed, restoreHeight); boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); if (!success) Log.e(TAG, newWallet.getErrorString()); newWallet.setPassword(password); @@ -397,9 +421,9 @@ public class LoginActivity extends AppCompatActivity final String address, final String viewKey, final String spendKey, final long restoreHeight) { createWallet(name, password, new WalletCreator() { - public boolean createWallet(String path, String password) { + public boolean createWallet(File aFile, String password) { Wallet newWallet = WalletManager.getInstance() - .createWalletFromKeys(path, MNEMONIC_LANGUAGE, restoreHeight, + .createWalletFromKeys(aFile, MNEMONIC_LANGUAGE, restoreHeight, address, viewKey, spendKey); boolean success = (newWallet.getStatus() == Wallet.Status.Status_Ok); if (!success) Log.e(TAG, newWallet.getErrorString()); diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index 4515ada..7a396d5 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -86,6 +86,8 @@ public class LoginFragment extends Fragment { void onWalletDetails(final String wallet); + void onWalletReceive(final String wallet); + void onAddWallet(); void setTitle(String title); @@ -408,17 +410,18 @@ public class LoginFragment extends Fragment { @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); + String listItem = (String) listView.getItemAtPosition(info.position); switch (item.getItemId()) { case R.id.action_info: - String listItem = (String) listView.getItemAtPosition(info.position); return showInfo(listItem); + case R.id.action_receive: + return showReceive(listItem); default: return super.onContextItemSelected(item); } } private boolean showInfo(String listItem) { - if (listItem.length() <= (WALLETNAME_PREAMBLE_LENGTH)) { Toast.makeText(getActivity(), getString(R.string.panic), Toast.LENGTH_LONG).show(); return true; @@ -436,4 +439,21 @@ public class LoginFragment extends Fragment { activityCallback.onWalletDetails(wallet); return true; } + + private boolean showReceive(String listItem) { + if (listItem.length() <= (WALLETNAME_PREAMBLE_LENGTH)) { + Toast.makeText(getActivity(), getString(R.string.panic), Toast.LENGTH_LONG).show(); + return true; + } + + String wallet = listItem.substring(WALLETNAME_PREAMBLE_LENGTH); + String x = isMainNet() ? "4" : "9A"; + if (x.indexOf(listItem.charAt(1)) < 0) { + Toast.makeText(getActivity(), getString(R.string.prompt_wrong_net), Toast.LENGTH_LONG).show(); + return true; + } + + activityCallback.onWalletReceive(wallet); + return true; + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java new file mode 100644 index 0000000..850fa0b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/ReceiveFragment.java @@ -0,0 +1,265 @@ +/* + * 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.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +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.model.Wallet; +import com.m2049r.xmrwallet.util.Helper; + +import java.util.HashMap; +import java.util.Map; + +public class ReceiveFragment extends Fragment { + static final String TAG = "ReceiveFragment"; + + TextView tvAddress; + EditText etPaymentId; + EditText etAmount; + Button bPaymentId; + Button bGenerate; + ImageView qrCode; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.receive_fragment, container, false); + + tvAddress = (TextView) view.findViewById(R.id.tvAddress); + etPaymentId = (EditText) view.findViewById(R.id.etPaymentId); + etAmount = (EditText) view.findViewById(R.id.etAmount); + bPaymentId = (Button) view.findViewById(R.id.bPaymentId); + qrCode = (ImageView) view.findViewById(R.id.qrCode); + bGenerate = (Button) view.findViewById(R.id.bGenerate); + + etPaymentId.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + + Helper.showKeyboard(getActivity()); + etPaymentId.requestFocus(); + etPaymentId.setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (paymentIdOk()) { + etAmount.requestFocus(); + } // otherwise ignore + return true; + } + return false; + } + }); + etPaymentId.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + qrCode.setImageBitmap(null); + if (paymentIdOk() && amountOk()) { + bGenerate.setEnabled(true); + } else { + bGenerate.setEnabled(false); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + + etAmount.setOnEditorActionListener(new TextView.OnEditorActionListener() { + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_NEXT)) { + if (paymentIdOk() && amountOk()) { + Helper.hideKeyboard(getActivity()); + generateQr(); + } + return true; + } + return false; + } + }); + etAmount.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable editable) { + qrCode.setImageBitmap(null); + if (paymentIdOk() && amountOk()) { + bGenerate.setEnabled(true); + } else { + bGenerate.setEnabled(false); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }); + + bPaymentId.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + etPaymentId.setText((Wallet.generatePaymentId())); + etPaymentId.setSelection(etPaymentId.getText().length()); + if (paymentIdOk() && amountOk()) { + generateQr(); + } + } + }); + + bGenerate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (paymentIdOk() && amountOk()) { + Helper.hideKeyboard(getActivity()); + generateQr(); + } + } + }); + + Bundle b = getArguments(); + String address = b.getString("address", ""); + tvAddress.setText(address); + return view; + } + + private boolean amountOk() { + String amountEntry = etAmount.getText().toString(); + if (amountEntry.isEmpty()) return true; + long amount = Wallet.getAmountFromString(amountEntry); + return (amount > 0); + } + + private boolean paymentIdOk() { + String paymentId = etPaymentId.getText().toString(); + return paymentId.isEmpty() || Wallet.isPaymentIdValid(paymentId); + } + + private void generateQr() { + String address = tvAddress.getText().toString(); + String paymentId = etPaymentId.getText().toString(); + String enteredAmount = etAmount.getText().toString(); + // that's a lot of converting ... + String amount = (enteredAmount.isEmpty()?enteredAmount:Helper.getDisplayAmount(Wallet.getAmountFromString(enteredAmount))); + StringBuffer sb = new StringBuffer(); + sb.append(ScannerFragment.QR_SCHEME).append(address); + boolean first = true; + if (!paymentId.isEmpty()) { + if (first) { + sb.append("?"); + first = false; + } + sb.append(ScannerFragment.QR_PAYMENTID).append('=').append(paymentId); + } + if (!amount.isEmpty()) { + if (first) { + sb.append("?"); + } else { + sb.append("&"); + } + sb.append(ScannerFragment.QR_AMOUNT).append('=').append(amount); + } + String text = sb.toString(); + Bitmap qr = generate(text, 500, 500); + if (qr != null) { + qrCode.setImageBitmap(qr); + } + } + + public Bitmap generate(String text, int width, int height) { + Map 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 e) { + e.printStackTrace(); + } + return null; + } + + private Bitmap addLogo(Bitmap qrBitmap) { + Bitmap logo = getMoneroLogo(); + int qrWidth = qrBitmap.getWidth(); + int qrHeight = qrBitmap.getHeight(); + int logoWidth = logo.getWidth(); + int logoHeight = logo.getHeight(); + + Bitmap logoBitmap = Bitmap.createBitmap(qrWidth, qrHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(logoBitmap); + canvas.drawBitmap(qrBitmap, 0, 0, null); + canvas.save(Canvas.ALL_SAVE_FLAG); + // figure out how to scale the logo + float scaleSize = 1.0f; + while ((logoWidth / scaleSize) > (qrWidth / 5) || (logoHeight / scaleSize) > (qrHeight / 5)) { + scaleSize *= 2; + } + float sx = 1.0f / scaleSize; + canvas.scale(sx, sx, qrWidth / 2, qrHeight / 2); + canvas.drawBitmap(logo, (qrWidth - logoWidth) / 2, (qrHeight - logoHeight) / 2, null); + canvas.restore(); + return logoBitmap; + } + + private Bitmap logo = null; + + private Bitmap getMoneroLogo() { + if (logo == null) { + logo = Helper.getBitmap(getContext(), R.drawable.ic_monero_qr); + } + return logo; + } + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java b/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java index 72be4a4..d5617d7 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/SendFragment.java @@ -100,8 +100,8 @@ public class SendFragment extends Fragment { pbProgress = (ProgressBar) view.findViewById(R.id.pbProgress); - etAddress.setRawInputType(InputType.TYPE_CLASS_TEXT); - etPaymentId.setRawInputType(InputType.TYPE_CLASS_TEXT); + etAddress.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + etPaymentId.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); etNotes.setRawInputType(InputType.TYPE_CLASS_TEXT); Helper.showKeyboard(getActivity()); @@ -209,7 +209,8 @@ public class SendFragment extends Fragment { bPaymentId.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - etPaymentId.setText((activityCallback.generatePaymentId())); + etPaymentId.setText((Wallet.generatePaymentId())); + etPaymentId.setSelection(etPaymentId.getText().length()); } }); @@ -285,7 +286,7 @@ public class SendFragment extends Fragment { private boolean paymentIdOk() { String paymentId = etPaymentId.getText().toString(); - return paymentId.isEmpty() || activityCallback.isPaymentIdValid(paymentId); + return paymentId.isEmpty() || Wallet.isPaymentIdValid(paymentId); } private void prepareSend() { @@ -358,10 +359,6 @@ public class SendFragment extends Fragment { void onSend(String notes); - String generatePaymentId(); - - boolean isPaymentIdValid(String paymentId); - String getWalletAddress(); void onDisposeRequest(); diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 9b71fae..36026f2 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -561,16 +561,6 @@ public class WalletActivity extends AppCompatActivity implements WalletFragment. } } - @Override - public String generatePaymentId() { - return getWallet().generatePaymentId(); - } - - @Override - public boolean isPaymentIdValid(String paymentId) { - return Wallet.isPaymentIdValid(paymentId); - } - @Override public String getWalletAddress() { return getWallet().getAddress(); diff --git a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java index 3cf5fb5..12bb13e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java +++ b/app/src/main/java/com/m2049r/xmrwallet/model/WalletManager.java @@ -71,8 +71,8 @@ public class WalletManager { managedWallets.remove(walletId); } - public Wallet createWallet(String path, String password, String language) { - long walletHandle = createWalletJ(path, password, language, isTestNet()); + public Wallet createWallet(File aFile, String password, String language) { + long walletHandle = createWalletJ(aFile.getAbsolutePath(), password, language, isTestNet()); Wallet wallet = new Wallet(walletHandle); manageWallet(wallet.getName(), wallet); return wallet; @@ -89,14 +89,14 @@ public class WalletManager { private native long openWalletJ(String path, String password, boolean isTestNet); - public Wallet recoveryWallet(String path, String mnemonic) { - Wallet wallet = recoveryWallet(path, mnemonic, 0); + public Wallet recoveryWallet(File aFile, String mnemonic) { + Wallet wallet = recoveryWallet(aFile, mnemonic, 0); manageWallet(wallet.getName(), wallet); return wallet; } - public Wallet recoveryWallet(String path, String mnemonic, long restoreHeight) { - long walletHandle = recoveryWalletJ(path, mnemonic, isTestNet(), restoreHeight); + public Wallet recoveryWallet(File aFile, String mnemonic, long restoreHeight) { + long walletHandle = recoveryWalletJ(aFile.getAbsolutePath(), mnemonic, isTestNet(), restoreHeight); Wallet wallet = new Wallet(walletHandle); manageWallet(wallet.getName(), wallet); return wallet; @@ -104,9 +104,9 @@ public class WalletManager { private native long recoveryWalletJ(String path, String mnemonic, boolean isTestNet, long restoreHeight); - public Wallet createWalletFromKeys(String path, String language, long restoreHeight, + public Wallet createWalletFromKeys(File aFile, String language, long restoreHeight, String addressString, String viewKeyString, String spendKeyString) { - long walletHandle = createWalletFromKeysJ(path, language, isTestNet(), restoreHeight, + long walletHandle = createWalletFromKeysJ(aFile.getAbsolutePath(), language, isTestNet(), restoreHeight, addressString, viewKeyString, spendKeyString); Wallet wallet = new Wallet(walletHandle); manageWallet(wallet.getName(), wallet); @@ -134,6 +134,10 @@ public class WalletManager { return closed; } + public boolean walletExists(File aFile) { + return walletExists(aFile.getAbsolutePath()); + } + public native boolean walletExists(String path); public native boolean verifyWalletPassword(String keys_file_name, String password, boolean watch_only); @@ -146,6 +150,31 @@ public class WalletManager { public String address; } + public WalletInfo getWalletInfo(File wallet) { + WalletInfo info = new WalletInfo(); + info.path = wallet.getParentFile(); + info.name = wallet.getName(); + File addressFile = new File(info.path, info.name + ".address.txt"); + //Log.d(TAG, addressFile.getAbsolutePath()); + info.address = "??????"; + BufferedReader addressReader = null; + try { + addressReader = new BufferedReader(new FileReader(addressFile)); + info.address = addressReader.readLine(); + } catch (IOException ex) { + Log.d(TAG, ex.getLocalizedMessage()); + } finally { + if (addressReader != null) { + try { + addressReader.close(); + } catch (IOException ex) { + // that's just too bad + } + } + } + return info; + } + public List findWallets(File path) { List wallets = new ArrayList<>(); Log.d(TAG, "Scanning: " + path.getAbsolutePath()); @@ -155,29 +184,9 @@ public class WalletManager { } }); for (int i = 0; i < found.length; i++) { - WalletInfo info = new WalletInfo(); - info.path = path; String filename = found[i].getName(); - info.name = filename.substring(0, filename.length() - 5); // 5 is length of ".keys"+1 - File addressFile = new File(path, info.name + ".address.txt"); - //Log.d(TAG, addressFile.getAbsolutePath()); - info.address = "??????"; - BufferedReader addressReader = null; - try { - addressReader = new BufferedReader(new FileReader(addressFile)); - info.address = addressReader.readLine(); - } catch (IOException ex) { - Log.d(TAG, ex.getLocalizedMessage()); - } finally { - if (addressReader != null) { - try { - addressReader.close(); - } catch (IOException ex) { - // that's just too bad - } - } - } - wallets.add(info); + File f = new File(found[i].getParent(), filename.substring(0, filename.length() - 5)); // 5 is length of ".keys"+1 + wallets.add(getWalletInfo(f)); } return wallets; } diff --git a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java index 5883604..7e6de93 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java +++ b/app/src/main/java/com/m2049r/xmrwallet/service/WalletService.java @@ -527,7 +527,7 @@ public class WalletService extends Service { } private Wallet openWallet(String walletName, String walletPassword) { - String path = Helper.getWalletPath(getApplicationContext(), walletName); + String path = Helper.getWalletFile(getApplicationContext(), walletName).getAbsolutePath(); showProgress(20); Wallet wallet = null; WalletManager walletMgr = WalletManager.getInstance(); @@ -557,7 +557,7 @@ public class WalletService extends Service { PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); Notification notification = new Notification.Builder(this) .setContentTitle(getString(R.string.service_description)) - .setSmallIcon(R.drawable.ic_notification_sync_32_32) + .setSmallIcon(R.drawable.ic_monero_32dp) .setContentIntent(pendingIntent) .build(); startForeground(NOTIFICATION_ID, notification); diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java index f2b6b2a..e3fdee3 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -21,7 +21,14 @@ import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.VectorDrawable; import android.os.Environment; +import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; @@ -91,12 +98,16 @@ public class Helper { } } - static public String getWalletPath(Context context, String aWalletName) { +// static public String getWalletPath(Context context, String aWalletName) { +// return getWalletFile(context, aWalletName).getAbsolutePath(); +// } + + static public File getWalletFile(Context context, String aWalletName) { File walletDir = getStorageRoot(context); //d(TAG, "walletdir=" + walletDir.getAbsolutePath()); File f = new File(walletDir, aWalletName); Log.d(TAG, "wallet = " + f.getAbsolutePath() + " size=" + f.length()); - return f.getAbsolutePath(); + return f; } /* Checks if external storage is available for read and write */ @@ -136,6 +147,7 @@ public class Helper { return ((address.length() == 95) && ("4".indexOf(address.charAt(0)) >= 0)); } } + static public String getDisplayAmount(long amount) { String s = Wallet.getDisplayAmount(amount); int lastZero = 0; @@ -152,4 +164,25 @@ public class Helper { int cutoff = Math.max(lastZero, decimal + 2); return s.substring(0, cutoff); } + + public static Bitmap getBitmap(Context context, int drawableId) { + Drawable drawable = ContextCompat.getDrawable(context, drawableId); + if (drawable instanceof BitmapDrawable) { + return BitmapFactory.decodeResource(context.getResources(), drawableId); + } else if (drawable instanceof VectorDrawable) { + return getBitmap((VectorDrawable) drawable); + } else { + throw new IllegalArgumentException("unsupported drawable type"); + } + } + + private static Bitmap getBitmap(VectorDrawable vectorDrawable) { + Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), + vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + vectorDrawable.draw(canvas); + return bitmap; + } + } diff --git a/app/src/main/res/drawable/ic_monero_32dp.xml b/app/src/main/res/drawable/ic_monero_32dp.xml new file mode 100644 index 0000000..acb4674 --- /dev/null +++ b/app/src/main/res/drawable/ic_monero_32dp.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_monero_qr.xml b/app/src/main/res/drawable/ic_monero_qr.xml new file mode 100644 index 0000000..0249bde --- /dev/null +++ b/app/src/main/res/drawable/ic_monero_qr.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_notification_sync_32_32.png b/app/src/main/res/drawable/ic_notification_sync_32_32.png deleted file mode 100644 index 4b881482e013e8c2f27b3a493b1ac68a42a75059..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1738 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 + + + + + + + + + + +