diff --git a/app/build.gradle b/app/build.gradle index 0ee925b..400d9ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,14 +1,14 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 29 + compileSdkVersion 30 buildToolsVersion '29.0.3' defaultConfig { applicationId "com.m2049r.xmrwallet" minSdkVersion 21 - targetSdkVersion 29 - versionCode 801 - versionName "1.18.1 'ChAdOx1'" + targetSdkVersion 30 + versionCode 900 + versionName "1.19.0.2 'XXX'" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5f9acf4..a18294f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ - diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index be6dd76..4e8895e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -23,10 +23,8 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; -import android.media.MediaScannerConnection; import android.os.AsyncTask; import android.os.Bundle; import android.view.KeyEvent; @@ -39,6 +37,7 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; import androidx.fragment.app.Fragment; @@ -62,10 +61,13 @@ import com.m2049r.xmrwallet.service.WalletService; import com.m2049r.xmrwallet.util.DayNightMode; import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.KeyStoreHelper; +import com.m2049r.xmrwallet.util.LegacyStorageHelper; import com.m2049r.xmrwallet.util.LocaleHelper; import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor; import com.m2049r.xmrwallet.util.NightmodeHelper; import com.m2049r.xmrwallet.util.ThemeHelper; +import com.m2049r.xmrwallet.util.ZipBackup; +import com.m2049r.xmrwallet.util.ZipRestore; import com.m2049r.xmrwallet.widget.Toolbar; import java.io.File; @@ -290,9 +292,6 @@ public class LoginActivity extends BaseActivity protected void onCreate(Bundle savedInstanceState) { Timber.d("onCreate()"); super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - // we don't store anything ourselves - } setContentView(R.layout.activity_login); toolbar = findViewById(R.id.toolbar); @@ -319,11 +318,9 @@ public class LoginActivity extends BaseActivity loadFavouritesWithNetwork(); - if (Helper.getWritePermission(this)) { - if (savedInstanceState == null) startLoginFragment(); - } else { - Timber.i("Waiting for permissions"); - } + LegacyStorageHelper.migrateWallets(this); + + if (savedInstanceState == null) startLoginFragment(); // try intents Intent intent = getIntent(); @@ -396,53 +393,25 @@ public class LoginActivity extends BaseActivity .show(); } - private class AsyncRename extends AsyncTask { - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.rename_progress); + private void renameWallet(String oldName, String newName) { + File walletFile = Helper.getWalletFile(this, oldName); + boolean success = renameWallet(walletFile, newName); + if (success) { + reloadWalletList(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.rename_failed), Toast.LENGTH_LONG).show(); } + } - @Override - protected Boolean doInBackground(String... params) { - if (params.length != 2) return false; - String oldName = params[0]; - String newName = params[1]; - File walletFile = Helper.getWalletFile(LoginActivity.this, oldName); - boolean success = renameWallet(walletFile, newName); + // copy + delete seems safer than rename because we can rollback easily + boolean renameWallet(File walletFile, String newName) { + if (copyWallet(walletFile, new File(walletFile.getParentFile(), newName), false, true)) { try { - if (success) { - String savedPass = KeyStoreHelper.loadWalletUserPass(LoginActivity.this, oldName); - KeyStoreHelper.saveWalletUserPass(LoginActivity.this, newName, savedPass); - } + KeyStoreHelper.copyWalletUserPass(this, walletFile.getName(), newName); } catch (KeyStoreHelper.BrokenPasswordStoreException ex) { Timber.w(ex); - } finally { - // we have either set a new password or it is broken - kill the old one either way - KeyStoreHelper.removeWalletUserPass(LoginActivity.this, oldName); - } - return success; - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); - if (result) { - reloadWalletList(); - } else { - Toast.makeText(LoginActivity.this, getString(R.string.rename_failed), Toast.LENGTH_LONG).show(); } - } - } - - // copy + delete seems safer than rename because we call rollback easily - boolean renameWallet(File walletFile, String newName) { - if (copyWallet(walletFile, new File(walletFile.getParentFile(), newName), false, true)) { - deleteWallet(walletFile); + deleteWallet(walletFile); // also deletes the keystore entry return true; } else { return false; @@ -468,154 +437,123 @@ public class LoginActivity extends BaseActivity alertDialogBuilder .setCancelable(false) .setPositiveButton(getString(R.string.label_ok), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Helper.hideKeyboardAlways(LoginActivity.this); - String newName = etRename.getText().toString(); - new AsyncRename().execute(walletName, newName); - } + (dialog, id) -> { + Helper.hideKeyboardAlways(LoginActivity.this); + String newName = etRename.getText().toString(); + renameWallet(walletName, newName); }) .setNegativeButton(getString(R.string.label_cancel), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - Helper.hideKeyboardAlways(LoginActivity.this); - dialog.cancel(); - } + (dialog, id) -> { + Helper.hideKeyboardAlways(LoginActivity.this); + dialog.cancel(); }); final AlertDialog dialog = alertDialogBuilder.create(); Helper.showKeyboard(dialog); // accept keyboard "ok" - etRename.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)) { - Helper.hideKeyboardAlways(LoginActivity.this); - String newName = etRename.getText().toString(); - dialog.cancel(); - new AsyncRename().execute(walletName, newName); - return false; - } + etRename.setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) + || (actionId == EditorInfo.IME_ACTION_DONE)) { + Helper.hideKeyboardAlways(LoginActivity.this); + String newName = etRename.getText().toString(); + dialog.cancel(); + renameWallet(walletName, newName); return false; } + return false; }); dialog.show(); } - private class AsyncBackup extends AsyncTask { - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.backup_progress); - } - - @Override - protected Boolean doInBackground(String... params) { - if (params.length != 1) return false; - return backupWallet(params[0]); - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (isDestroyed()) { - return; - } - dismissProgressDialog(); - if (!result) { - Toast.makeText(LoginActivity.this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(LoginActivity.this, getString(R.string.backup_success), Toast.LENGTH_SHORT).show(); - } - } - } - - private boolean backupWallet(String walletName) { - File backupFolder = new File(getStorageRoot(), "backups"); - if (!backupFolder.exists()) { - if (!backupFolder.mkdir()) { - Timber.e("Cannot create backup dir %s", backupFolder.getAbsolutePath()); - return false; - } - // make folder visible over USB/MTP - MediaScannerConnection.scanFile(this, new String[]{backupFolder.toString()}, null, null); - } - File walletFile = Helper.getWalletFile(LoginActivity.this, walletName); - File backupFile = new File(backupFolder, walletName); - Timber.d("backup " + walletFile.getAbsolutePath() + " to " + backupFile.getAbsolutePath()); - // TODO probably better to copy to a new file and then rename - // then if something fails we have the old backup at least - // or just create a new backup every time and keep n old backups - boolean success = copyWallet(walletFile, backupFile, true, true); - Timber.d("copyWallet is %s", success); - return success; - } + private static final int CREATE_BACKUP_INTENT = 4711; + private static final int RESTORE_BACKUP_INTENT = 4712; + private ZipBackup zipBackup; @Override public void onWalletBackup(String walletName) { Timber.d("backup for wallet ." + walletName + "."); - new AsyncBackup().execute(walletName); + zipBackup = new ZipBackup(this, walletName); + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/zip"); + intent.putExtra(Intent.EXTRA_TITLE, zipBackup.getBackupName()); + startActivityForResult(intent, CREATE_BACKUP_INTENT); } - private class AsyncArchive extends AsyncTask { - @Override - protected void onPreExecute() { - super.onPreExecute(); - showProgressDialog(R.string.archive_progress); - } + @Override + public void onWalletRestore() { + Timber.d("restore wallet"); - @Override - protected Boolean doInBackground(String... params) { - if (params.length != 1) return false; - String walletName = params[0]; - if (backupWallet(walletName) && deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) { - KeyStoreHelper.removeWalletUserPass(LoginActivity.this, walletName); - return true; - } else { - return false; - } - } + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/zip"); + startActivityForResult(intent, RESTORE_BACKUP_INTENT); + } - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - if (isDestroyed()) { + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CREATE_BACKUP_INTENT) { + if (data == null) { + // nothing selected + Toast.makeText(this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); return; } - dismissProgressDialog(); - if (result) { - reloadWalletList(); - } else { - Toast.makeText(LoginActivity.this, getString(R.string.archive_failed), Toast.LENGTH_LONG).show(); + try { + zipBackup.writeTo(data.getData()); + Toast.makeText(this, getString(R.string.backup_success), Toast.LENGTH_SHORT).show(); + } catch (IOException ex) { + Timber.e(ex); + Toast.makeText(this, getString(R.string.backup_failed), Toast.LENGTH_LONG).show(); + } + } else if (requestCode == RESTORE_BACKUP_INTENT) { + if (data == null) { + // nothing selected + Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show(); + return; + } + try { + ZipRestore zipRestore = new ZipRestore(this, data.getData()); + Toast.makeText(this, getString(R.string.menu_restore), Toast.LENGTH_SHORT).show(); + if (zipRestore.restore()) { + reloadWalletList(); + } else { + Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show(); + } + } catch (IOException ex) { + Timber.e(ex); + Toast.makeText(this, getString(R.string.restore_failed), Toast.LENGTH_LONG).show(); } } } @Override - public void onWalletArchive(final String walletName) { - Timber.d("archive for wallet ." + walletName + "."); + public void onWalletDelete(final String walletName) { + Timber.d("delete for wallet ." + walletName + "."); if (checkServiceRunning()) return; - DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - new AsyncArchive().execute(walletName); - break; - case DialogInterface.BUTTON_NEGATIVE: - // do nothing - break; - } + DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + if (deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) { + reloadWalletList(); + } else { + Toast.makeText(LoginActivity.this, getString(R.string.delete_failed), Toast.LENGTH_LONG).show(); + } + break; + case DialogInterface.BUTTON_NEGATIVE: + // do nothing + break; } }; AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); - builder.setMessage(getString(R.string.archive_alert_message)) + builder.setMessage(getString(R.string.delete_alert_message)) .setTitle(walletName) - .setPositiveButton(getString(R.string.archive_alert_yes), dialogClickListener) - .setNegativeButton(getString(R.string.archive_alert_no), dialogClickListener) + .setPositiveButton(getString(R.string.delete_alert_yes), dialogClickListener) + .setNegativeButton(getString(R.string.delete_alert_no), dialogClickListener) .show(); } @@ -628,6 +566,7 @@ public class LoginActivity extends BaseActivity loginFragment.loadList(); } } catch (ClassCastException ex) { + Timber.w(ex); } } @@ -774,37 +713,6 @@ public class LoginActivity extends BaseActivity startReviewFragment(b); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], - @NonNull int[] grantResults) { - Timber.d("onRequestPermissionsResult()"); - switch (requestCode) { - case Helper.PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - startLoginFragment = true; - } else { - String msg = getString(R.string.message_strorage_not_permitted); - Timber.e(msg); - Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); - } - break; - default: - } - } - - private boolean startLoginFragment = false; - - @Override - protected void onResumeFragments() { - super.onResumeFragments(); - if (startLoginFragment) { - startLoginFragment(); - startLoginFragment = false; - } - } - void startLoginFragment() { // we set these here because we cannot be ceratin we have permissions for storage before Helper.setMoneroHome(this); @@ -1035,22 +943,12 @@ public class LoginActivity extends BaseActivity }); } - void toast(final String msg) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_LONG).show(); - } - }); + private void toast(final String msg) { + runOnUiThread(() -> Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_LONG).show()); } - void toast(final int msgId) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(LoginActivity.this, getString(msgId), Toast.LENGTH_LONG).show(); - } - }); + private void toast(final int msgId) { + runOnUiThread(() -> Toast.makeText(LoginActivity.this, getString(msgId), Toast.LENGTH_LONG).show()); } @Override @@ -1058,7 +956,7 @@ public class LoginActivity extends BaseActivity File walletFolder = getStorageRoot(); File walletFile = new File(walletFolder, name); Timber.d("New Wallet %s", walletFile.getAbsolutePath()); - walletFile.delete(); // when recovering wallets, the cache seems corrupt + walletFile.delete(); // when recovering wallets, the cache seems corrupt - so remove it popFragmentStack(GENERATE_STACK); Toast.makeText(LoginActivity.this, @@ -1079,8 +977,7 @@ public class LoginActivity extends BaseActivity } } - boolean copyWallet(File srcWallet, File dstWallet, boolean overwrite, - boolean ignoreCacheError) { + boolean copyWallet(File srcWallet, File dstWallet, boolean overwrite, boolean ignoreCacheError) { if (walletExists(dstWallet, true) && !overwrite) return false; boolean success = false; File srcDir = srcWallet.getParentFile(); @@ -1123,19 +1020,14 @@ public class LoginActivity extends BaseActivity success = addressFile.delete() && success; } Timber.d("deleteWallet is %s", success); + KeyStoreHelper.removeWalletUserPass(this, walletFile.getName()); return success; } void copyFile(File src, File dst) throws IOException { - FileChannel inChannel = new FileInputStream(src).getChannel(); - FileChannel outChannel = new FileOutputStream(dst).getChannel(); - try { + try (FileChannel inChannel = new FileInputStream(src).getChannel(); + FileChannel outChannel = new FileOutputStream(dst).getChannel()) { inChannel.transferTo(0, inChannel.size(), outChannel); - } finally { - if (inChannel != null) - inChannel.close(); - if (outChannel != null) - outChannel.close(); } } @@ -1220,63 +1112,64 @@ public class LoginActivity extends BaseActivity @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_create_help_new: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_new); - return true; - case R.id.action_create_help_keys: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_keys); - return true; - case R.id.action_create_help_view: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_view); - return true; - case R.id.action_create_help_seed: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_seed); - return true; - case R.id.action_create_help_ledger: - HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger); - return true; - case R.id.action_details_help: - HelpFragment.display(getSupportFragmentManager(), R.string.help_details); - return true; - case R.id.action_details_changepw: - onWalletChangePassword(); - return true; - case R.id.action_license_info: - AboutFragment.display(getSupportFragmentManager()); - return true; - case R.id.action_help_list: - HelpFragment.display(getSupportFragmentManager(), R.string.help_list); - return true; - case R.id.action_help_node: - HelpFragment.display(getSupportFragmentManager(), R.string.help_node); - return true; - case R.id.action_default_nodes: { - Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); - if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) && - (f instanceof NodeFragment)) { - ((NodeFragment) f).restoreDefaultNodes(); - } - return true; + final int id = item.getItemId(); + if (id == R.id.action_create_help_new) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_new); + return true; + } else if (id == R.id.action_create_help_keys) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_keys); + return true; + } else if (id == R.id.action_create_help_view) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_view); + return true; + } else if (id == R.id.action_create_help_seed) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_seed); + return true; + } else if (id == R.id.action_create_help_ledger) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger); + return true; + } else if (id == R.id.action_details_help) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_details); + return true; + } else if (id == R.id.action_details_changepw) { + onWalletChangePassword(); + return true; + } else if (id == R.id.action_license_info) { + AboutFragment.display(getSupportFragmentManager()); + return true; + } else if (id == R.id.action_help_list) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_list); + return true; + } else if (id == R.id.action_help_node) { + HelpFragment.display(getSupportFragmentManager(), R.string.help_node); + return true; + } else if (id == R.id.action_default_nodes) { + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) && + (f instanceof NodeFragment)) { + ((NodeFragment) f).restoreDefaultNodes(); } - case R.id.action_privacy_policy: - PrivacyFragment.display(getSupportFragmentManager()); - return true; - case R.id.action_language: - onChangeLocale(); - return true; - case R.id.action_theme: - onChangeTheme(); - return true; - case R.id.action_ledger_seed: { - Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); - if (f instanceof GenerateFragment) { - ((GenerateFragment) f).convertLedgerSeed(); - } - return true; + return true; + } else if (id == R.id.action_privacy_policy) { + PrivacyFragment.display(getSupportFragmentManager()); + return true; + } else if (id == R.id.action_language) { + onChangeLocale(); + return true; + } else if (id == R.id.action_theme) { + onChangeTheme(); + return true; + } else if (id == R.id.action_restore) { + onWalletRestore(); + return true; + } else if (id == R.id.action_ledger_seed) { + Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container); + if (f instanceof GenerateFragment) { + ((GenerateFragment) f).convertLedgerSeed(); } - default: - return super.onOptionsItemSelected(item); + return true; + } else { + return super.onOptionsItemSelected(item); } } @@ -1317,7 +1210,7 @@ public class LoginActivity extends BaseActivity if (result) { Timber.d("selected wallet is .%s.", node.getName()); // now it's getting real, onValidateFields if wallet exists - promptAndStart(walletName, node, streetmode); + promptAndStart(walletName, streetmode); } else { if (node.getResponseCode() == 0) { // IOException Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show(); @@ -1333,25 +1226,21 @@ public class LoginActivity extends BaseActivity String keyPath = new File(Helper.getWalletRoot(LoginActivity.this), walletName + ".keys").getAbsolutePath(); // check if we need connected hardware - Wallet.Device device = - WalletManager.getInstance().queryWalletDevice(keyPath, password); - switch (device) { - case Device_Ledger: - if (!hasLedger()) { - toast(R.string.open_wallet_ledger_missing); - } else { - return true; - } - break; - default: - // device could be undefined meaning the password is wrong - // this gets dealt with later + Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password); + if (device == Wallet.Device.Device_Ledger) { + if (!hasLedger()) { + toast(R.string.open_wallet_ledger_missing); + } else { return true; + } + } else {// device could be undefined meaning the password is wrong + // this gets dealt with later + return true; } return false; } - void promptAndStart(String walletName, Node node, final boolean streetmode) { + void promptAndStart(String walletName, final boolean streetmode) { File walletFile = Helper.getWalletFile(this, walletName); if (WalletManager.getInstance().walletExists(walletFile)) { Helper.promptPassword(LoginActivity.this, walletName, false, @@ -1418,38 +1307,23 @@ public class LoginActivity extends BaseActivity Ledger.connect(usbManager, usbDevice); if (!Ledger.check()) { Ledger.disconnect(); - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(LoginActivity.this, - getString(R.string.toast_ledger_start_app, usbDevice.getProductName()), - Toast.LENGTH_SHORT) - .show(); - } - }); + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_start_app, usbDevice.getProductName()), + Toast.LENGTH_SHORT) + .show()); } else { registerDetachReceiver(); onLedgerAction(); - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(LoginActivity.this, - getString(R.string.toast_ledger_attached, usbDevice.getProductName()), - Toast.LENGTH_SHORT) - .show(); - } - }); + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_attached, usbDevice.getProductName()), + Toast.LENGTH_SHORT) + .show()); } } catch (IOException ex) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(LoginActivity.this, - getString(R.string.open_wallet_ledger_missing), - Toast.LENGTH_SHORT) - .show(); - } - }); + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.open_wallet_ledger_missing), + Toast.LENGTH_SHORT) + .show()); } } @@ -1507,15 +1381,10 @@ public class LoginActivity extends BaseActivity final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); Timber.i("Ledger detached!"); if (device != null) - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(LoginActivity.this, - getString(R.string.toast_ledger_detached, device.getProductName()), - Toast.LENGTH_SHORT) - .show(); - } - }); + runOnUiThread(() -> Toast.makeText(LoginActivity.this, + getString(R.string.toast_ledger_detached, device.getProductName()), + Toast.LENGTH_SHORT) + .show()); Ledger.disconnect(); onLedgerAction(); } diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index e6caa3f..0db596e 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -31,7 +31,6 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -62,8 +61,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter private WalletInfoAdapter adapter; - private List walletList = new ArrayList<>(); - private List displayedList = new ArrayList<>(); + private final List walletList = new ArrayList<>(); private View tvGuntherSays; private ImageView ivGunther; @@ -86,7 +84,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter void onWalletBackup(String name); - void onWalletArchive(String walletName); + void onWalletRestore(); + + void onWalletDelete(String walletName); void onAddWallet(String type); @@ -110,7 +110,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter } @Override - public void onAttach(Context context) { + public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof Listener) { this.activityCallback = (Listener) context; @@ -200,12 +200,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter // Wallet touched @Override public void onInteraction(final View view, final WalletManager.WalletInfo infoItem) { - String addressPrefix = WalletManager.getInstance().addressPrefix(); - if (addressPrefix.indexOf(infoItem.address.charAt(0)) < 0) { - Toast.makeText(getActivity(), getString(R.string.prompt_wrong_net), Toast.LENGTH_LONG).show(); - return; - } - openWallet(infoItem.name, false); + openWallet(infoItem.getName(), false); } private void openWallet(String name, boolean streetmode) { @@ -214,48 +209,32 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter @Override public boolean onContextInteraction(MenuItem item, WalletManager.WalletInfo listItem) { - switch (item.getItemId()) { - case R.id.action_streetmode: - openWallet(listItem.name, true); - break; - case R.id.action_info: - showInfo(listItem.name); - break; - case R.id.action_rename: - activityCallback.onWalletRename(listItem.name); - break; - case R.id.action_backup: - activityCallback.onWalletBackup(listItem.name); - break; - case R.id.action_archive: - activityCallback.onWalletArchive(listItem.name); - break; - default: - return super.onContextItemSelected(item); + 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 { + return super.onContextItemSelected(item); } return true; } - private void filterList() { - displayedList.clear(); - String addressPrefix = WalletManager.getInstance().addressPrefix(); - for (WalletManager.WalletInfo s : walletList) { - if (addressPrefix.indexOf(s.address.charAt(0)) >= 0) displayedList.add(s); - } - } - public void loadList() { Timber.d("loadList()"); WalletManager mgr = WalletManager.getInstance(); - List walletInfos = - mgr.findWallets(activityCallback.getStorageRoot()); walletList.clear(); - walletList.addAll(walletInfos); - filterList(); - adapter.setInfos(displayedList); + walletList.addAll(mgr.findWallets(activityCallback.getStorageRoot())); + adapter.setInfos(walletList); // deal with Gunther & FAB animation - if (displayedList.isEmpty()) { + if (walletList.isEmpty()) { fab.startAnimation(fab_pulse); if (ivGunther.getDrawable() == null) { ivGunther.setImageResource(R.drawable.ic_emptygunther); @@ -274,7 +253,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter .getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE) .getAll().keySet(); for (WalletManager.WalletInfo s : walletList) { - removedWallets.remove(s.name); + removedWallets.remove(s.getName()); } for (String name : removedWallets) { KeyStoreHelper.removeWalletUserPass(getActivity(), name); @@ -292,7 +271,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.list_menu, menu); super.onCreateOptionsMenu(menu, inflater); } @@ -362,37 +341,29 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter @Override public void onClick(View v) { - int id = v.getId(); + final int id = v.getId(); Timber.d("onClick %d/%d", id, R.id.fabLedger); - switch (id) { - case R.id.fab: - animateFAB(); - break; - case R.id.fabNew: - fabScreen.setVisibility(View.INVISIBLE); - isFabOpen = false; - activityCallback.onAddWallet(GenerateFragment.TYPE_NEW); - break; - case R.id.fabView: - animateFAB(); - activityCallback.onAddWallet(GenerateFragment.TYPE_VIEWONLY); - break; - case R.id.fabKey: - animateFAB(); - activityCallback.onAddWallet(GenerateFragment.TYPE_KEY); - break; - case R.id.fabSeed: - animateFAB(); - activityCallback.onAddWallet(GenerateFragment.TYPE_SEED); - break; - case R.id.fabLedger: - Timber.d("FAB_LEDGER"); - animateFAB(); - activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER); - break; - case R.id.fabScreen: - animateFAB(); - break; + 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.fabLedger) { + Timber.d("FAB_LEDGER"); + animateFAB(); + activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER); + } else if (id == R.id.fabScreen) { + animateFAB(); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java index b9ad73c..ad885e7 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java +++ b/app/src/main/java/com/m2049r/xmrwallet/layout/WalletInfoAdapter.java @@ -74,7 +74,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter { if (popupOpen) return; @@ -160,8 +158,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter findWallets(String path); // this does not work - some error in boost public class WalletInfo implements Comparable { - public File path; - public String name; - public String address; + @Getter + final private File path; + @Getter + final private String name; + + public WalletInfo(File wallet) { + path = wallet.getParentFile(); + name = wallet.getName(); + } @Override public int compareTo(WalletInfo another) { - int n = name.toLowerCase().compareTo(another.name.toLowerCase()); - if (n != 0) { - return n; - } else { // wallet names are the same - return address.compareTo(another.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"); - //Timber.d(addressFile.getAbsolutePath()); - info.address = "??????"; - BufferedReader addressReader = null; - try { - addressReader = new BufferedReader(new FileReader(addressFile)); - info.address = addressReader.readLine(); - } catch (IOException ex) { - Timber.d(ex.getLocalizedMessage()); - } finally { - if (addressReader != null) { - try { - addressReader.close(); - } catch (IOException ex) { - // that's just too bad - } - } + return name.toLowerCase().compareTo(another.name.toLowerCase()); } - return info; } public List findWallets(File path) { @@ -261,7 +236,7 @@ public class WalletManager { for (int i = 0; i < found.length; i++) { String filename = found[i].getName(); File f = new File(found[i].getParent(), filename.substring(0, filename.length() - 5)); // 5 is length of ".keys"+1 - wallets.add(getWalletInfo(f)); + wallets.add(new WalletInfo(f)); } return wallets; } 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 2428cb3..e500e8c 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -34,7 +34,6 @@ import android.hardware.fingerprint.FingerprintManager; import android.os.AsyncTask; import android.os.Build; import android.os.CancellationSignal; -import android.os.Environment; import android.os.StrictMode; import android.system.ErrnoException; import android.system.Os; @@ -77,16 +76,12 @@ import javax.net.ssl.HttpsURLConnection; import timber.log.Timber; public class Helper { - static private final String FLAVOR_SUFFIX = - (BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR) - + (BuildConfig.DEBUG ? "-debug" : ""); - static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass"; static public final String BASE_CRYPTO = "XMR"; - static private final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX; - static private final String HOME_DIR = "monero" + FLAVOR_SUFFIX; + static private final String WALLET_DIR = "wallets"; + static private final String MONERO_DIR = "monero"; static public int DISPLAY_DIGITS_INFO = 5; @@ -95,12 +90,7 @@ public class Helper { } static public File getStorage(Context context, String folderName) { - if (!isExternalStorageWritable()) { - String msg = context.getString(R.string.message_strorage_not_writable); - Timber.e(msg); - throw new IllegalStateException(msg); - } - File dir = new File(Environment.getExternalStorageDirectory(), folderName); + File dir = new File(context.getFilesDir(), folderName); if (!dir.exists()) { Timber.i("Creating %s", dir.getAbsolutePath()); dir.mkdirs(); // try to make it @@ -113,24 +103,6 @@ public class Helper { return dir; } - static public final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1; - - static public boolean getWritePermission(Activity context) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - if (context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_DENIED) { - Timber.w("Permission denied to WRITE_EXTERNAL_STORAGE - requesting it"); - String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE}; - context.requestPermissions(permissions, PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE); - return false; - } else { - return true; - } - } else { - return true; - } - } - static public final int PERMISSIONS_REQUEST_CAMERA = 7; static public boolean getCameraPermission(Activity context) { @@ -156,12 +128,6 @@ public class Helper { return f; } - /* Checks if external storage is available for read and write */ - private static boolean isExternalStorageWritable() { - String state = Environment.getExternalStorageState(); - return Environment.MEDIA_MOUNTED.equals(state); - } - static public void showKeyboard(Activity act) { InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE); final View focus = act.getCurrentFocus(); @@ -339,7 +305,7 @@ public class Helper { static public void setMoneroHome(Context context) { try { - String home = getStorage(context, HOME_DIR).getAbsolutePath(); + String home = getStorage(context, MONERO_DIR).getAbsolutePath(); Os.setenv("HOME", home, true); } catch (ErrnoException ex) { throw new IllegalStateException(ex); @@ -355,7 +321,7 @@ public class Helper { // TODO make the log levels refer to the WalletManagerFactory::LogLevel enum ? static public void initLogger(Context context, int level) { - String home = getStorage(context, HOME_DIR).getAbsolutePath(); + String home = getStorage(context, MONERO_DIR).getAbsolutePath(); WalletManager.initLogger(home + "/monerujo", "monerujo.log"); if (level >= WalletManager.LOGLEVEL_SILENT) WalletManager.setLogLevel(level); diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java index 7c08f45..896d2d0 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java @@ -24,9 +24,10 @@ import android.os.Build; import android.security.KeyPairGeneratorSpec; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; -import androidx.annotation.NonNull; import android.util.Base64; +import androidx.annotation.NonNull; + import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; @@ -167,6 +168,11 @@ public class KeyStoreHelper { .remove(wallet).apply(); } + public static void copyWalletUserPass(Context context, String srcWallet, String dstWallet) throws BrokenPasswordStoreException { + final String pass = loadWalletUserPass(context, srcWallet); + saveWalletUserPass(context, dstWallet, pass); + } + /** * Creates a public and private key and stores it using the Android Key * Store, so that only this application will be able to access the keys. diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/LegacyStorageHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/LegacyStorageHelper.java new file mode 100644 index 0000000..60bbf4e --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/LegacyStorageHelper.java @@ -0,0 +1,170 @@ +package com.m2049r.xmrwallet.util; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.preference.PreferenceManager; + +import com.m2049r.xmrwallet.BuildConfig; +import com.m2049r.xmrwallet.model.WalletManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +@RequiredArgsConstructor +public class LegacyStorageHelper { + final private File srcDir; + final private File dstDir; + + static public void migrateWallets(Context context) { + try { + if (isStorageMigrated(context)) return; + if (!hasWritePermission(context)) { + // nothing to migrate, so don't try again + setStorageMigrated(context); + return; + } + final File oldRoot = getWalletRoot(); + if (!oldRoot.exists()) { + // nothing to migrate, so don't try again + setStorageMigrated(context); + return; + } + final File newRoot = Helper.getWalletRoot(context); + (new LegacyStorageHelper(oldRoot, newRoot)).migrate(); + setStorageMigrated(context); // done it once - don't try again + } catch (IllegalStateException ex) { + Timber.d(ex); + // nothing we can do here + } + } + + public void migrate() { + String addressPrefix = WalletManager.getInstance().addressPrefix(); + File[] wallets = srcDir.listFiles((dir, filename) -> filename.endsWith(".keys")); + for (File wallet : wallets) { + final String walletName = wallet.getName().substring(0, wallet.getName().length() - ".keys".length()); + if (addressPrefix.indexOf(getAddress(walletName).charAt(0)) < 0) { + Timber.d("skipping %s", walletName); + continue; + } + try { + copy(walletName); + } catch (IOException ex) { // something failed - try to clean up + deleteDst(walletName); + } + } + } + + // return "@" by default so we don't need to deal with null stuff + private String getAddress(String walletName) { + File addressFile = new File(srcDir, walletName + ".address.txt"); + if (!addressFile.exists()) return "@"; + try (BufferedReader addressReader = new BufferedReader(new FileReader(addressFile))) { + return addressReader.readLine(); + } catch (IOException ex) { + Timber.d(ex.getLocalizedMessage()); + } + return "@"; + } + + private void copy(String walletName) throws IOException { + final String dstName = getUniqueName(dstDir, walletName); + copyFile(new File(srcDir, walletName), new File(dstDir, dstName)); + copyFile(new File(srcDir, walletName + ".keys"), new File(dstDir, dstName + ".keys")); + } + + private void deleteDst(String walletName) { + // do our best, but if it fails, it fails + (new File(dstDir, walletName)).delete(); + (new File(dstDir, walletName + ".keys")).delete(); + } + + private void copyFile(File src, File dst) throws IOException { + if (!src.exists()) return; + Timber.d("%s => %s", src.getAbsolutePath(), dst.getAbsolutePath()); + try (FileChannel inChannel = new FileInputStream(src).getChannel(); + FileChannel outChannel = new FileOutputStream(dst).getChannel()) { + inChannel.transferTo(0, inChannel.size(), outChannel); + } + } + + private static boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } + + private static File getWalletRoot() { + if (!isExternalStorageWritable()) + throw new IllegalStateException(); + + // wallet folder for legacy (pre-Q) installations + final String FLAVOR_SUFFIX = + (BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR) + + (BuildConfig.DEBUG ? "-debug" : ""); + final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX; + + File dir = new File(Environment.getExternalStorageDirectory(), WALLET_DIR); + if (!dir.exists() || !dir.isDirectory()) + throw new IllegalStateException(); + return dir; + } + + private static boolean hasWritePermission(Context context) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + return context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_DENIED; + } else { + return true; + } + } + + private static final Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$"); + + private static String getUniqueName(File root, String name) { + if (!(new File(root, name + ".keys")).exists()) // does not exist => it's ok to use + return name; + + File[] wallets = root.listFiles( + (dir, filename) -> { + Matcher m = WALLET_PATTERN.matcher(filename); + if (m.find()) + return m.group(1).equals(name); + else return false; + }); + if (wallets.length == 0) return name + " (1)"; + int maxIndex = 0; + for (File wallet : wallets) { + try { + final Matcher m = WALLET_PATTERN.matcher(wallet.getName()); + if (!m.find()) + throw new IllegalStateException("this must match as it did before"); + final int index = Integer.parseInt(m.group(2)); + if (index > maxIndex) maxIndex = index; + } catch (NumberFormatException ex) { + // this cannot happen & we can ignore it if it does + } + } + return name + " (" + (maxIndex + 1) + ")"; + } + + private static final String MIGRATED_KEY = "migrated_legacy_storage"; + + public static boolean isStorageMigrated(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(MIGRATED_KEY, false); + } + + public static void setStorageMigrated(Context context) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(MIGRATED_KEY, true).apply(); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ZipBackup.java b/app/src/main/java/com/m2049r/xmrwallet/util/ZipBackup.java new file mode 100644 index 0000000..cfdc705 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ZipBackup.java @@ -0,0 +1,64 @@ +package com.m2049r.xmrwallet.util; + +import android.content.Context; +import android.net.Uri; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ZipBackup { + final private Context context; + final private String walletName; + + private ZipOutputStream zip; + + public void writeTo(Uri zipUri) throws IOException { + if (zip != null) + throw new IllegalStateException("zip already initialized"); + try { + zip = new ZipOutputStream(context.getContentResolver().openOutputStream(zipUri)); + + final File walletRoot = Helper.getWalletRoot(context); + addFile(new File(walletRoot, walletName + ".keys")); + addFile(new File(walletRoot, walletName)); + + zip.close(); + } finally { + if (zip != null) zip.close(); + } + } + + private void addFile(File file) throws IOException { + if (!file.exists()) return; // ignore missing files (e.g. the cache file might not exist) + ZipEntry entry = new ZipEntry(file.getName()); + zip.putNextEntry(entry); + writeFile(file); + zip.closeEntry(); + } + + private void writeFile(File source) throws IOException { + try (InputStream is = new FileInputStream(source)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = is.read(buffer)) > 0) { + zip.write(buffer, 0, length); + } + } + } + + private static final SimpleDateFormat DATETIME_FORMATTER = + new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss"); + + public String getBackupName() { + return walletName + " " + DATETIME_FORMATTER.format(new Date()); + } +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/ZipRestore.java b/app/src/main/java/com/m2049r/xmrwallet/util/ZipRestore.java new file mode 100644 index 0000000..1b9148b --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/ZipRestore.java @@ -0,0 +1,139 @@ +package com.m2049r.xmrwallet.util; + +import android.content.Context; +import android.net.Uri; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import lombok.RequiredArgsConstructor; +import timber.log.Timber; + +@RequiredArgsConstructor +public class ZipRestore { + final private Context context; + final private Uri zipUri; + + private File walletRoot; + + private ZipInputStream zip; + + public boolean restore() throws IOException { + walletRoot = Helper.getWalletRoot(context); + String walletName = testArchive(); + if (walletName == null) return false; + + walletName = getUniqueName(walletName); + + if (zip != null) + throw new IllegalStateException("zip already initialized"); + try { + zip = new ZipInputStream(context.getContentResolver().openInputStream(zipUri)); + for (ZipEntry entry = zip.getNextEntry(); entry != null; zip.closeEntry(), entry = zip.getNextEntry()) { + File destination; + final String name = entry.getName(); + if (name.endsWith(".keys")) { + destination = new File(walletRoot, walletName + ".keys"); + } else if (name.endsWith(".address.txt")) { + destination = new File(walletRoot, walletName + ".address.txt"); + } else { + destination = new File(walletRoot, walletName); + } + writeFile(destination); + } + } finally { + if (zip != null) zip.close(); + } + return true; + } + + private void writeFile(File destination) throws IOException { + try (OutputStream os = new FileOutputStream(destination)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = zip.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + } + } + + // test the archive to contain files we expect & return the name of the contained wallet or null + private String testArchive() { + String walletName = null; + boolean keys = false; + ZipInputStream zipStream = null; + try { + zipStream = new ZipInputStream(context.getContentResolver().openInputStream(zipUri)); + for (ZipEntry entry = zipStream.getNextEntry(); entry != null; + zipStream.closeEntry(), entry = zipStream.getNextEntry()) { + if (entry.isDirectory()) + return null; + final String name = entry.getName(); + if ((new File(name)).getParentFile() != null) + return null; + if (walletName == null) { + if (name.endsWith(".keys")) { + walletName = name.substring(0, name.length() - ".keys".length()); + keys = true; // we have they keys + } else if (name.endsWith(".address.txt")) { + walletName = name.substring(0, name.length() - ".address.txt".length()); + } else { + walletName = name; + } + } else { // we have a wallet name + if (name.endsWith(".keys")) { + if (!name.equals(walletName + ".keys")) return null; + keys = true; // we have they keys + } else if (name.endsWith(".address.txt")) { + if (!name.equals(walletName + ".address.txt")) return null; + } else if (!name.equals(walletName)) return null; + } + } + } catch (IOException ex) { + return null; + } finally { + try { + if (zipStream != null) zipStream.close(); + } catch (IOException ex) { + Timber.w(ex); + } + } + // we need the keys at least + if (keys) return walletName; + else return null; + } + + final static Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$"); + + private String getUniqueName(String name) { + if (!(new File(walletRoot, name + ".keys")).exists()) // does not exist => it's ok to use + return name; + + File[] wallets = walletRoot.listFiles( + (dir, filename) -> { + Matcher m = WALLET_PATTERN.matcher(filename); + if (m.find()) + return m.group(1).equals(name); + else return false; + }); + if (wallets.length == 0) return name + " (1)"; + int maxIndex = 0; + for (File wallet : wallets) { + try { + final Matcher m = WALLET_PATTERN.matcher(wallet.getName()); + m.find(); + final int index = Integer.parseInt(m.group(2)); + if (index > maxIndex) maxIndex = index; + } catch (NumberFormatException ex) { + // this cannot happen & we can ignore it if it does + } + } + return name + " (" + (maxIndex + 1) + ")"; + } +} diff --git a/app/src/main/res/layout/item_wallet.xml b/app/src/main/res/layout/item_wallet.xml index 30e3927..0b918a1 100644 --- a/app/src/main/res/layout/item_wallet.xml +++ b/app/src/main/res/layout/item_wallet.xml @@ -18,19 +18,9 @@ android:layout_height="wrap_content" android:layout_marginStart="48dp" android:layout_marginTop="16dp" - android:layout_toStartOf="@+id/ibOptions" - tools:text="My Wallet" /> - - + tools:text="My Wallet" /> + android:title="@string/menu_delete" /> + \ No newline at end of file diff --git a/app/src/main/res/values-cat/strings.xml b/app/src/main/res/values-cat/strings.xml index f3240ec..6c517c6 100644 --- a/app/src/main/res/values-cat/strings.xml +++ b/app/src/main/res/values-cat/strings.xml @@ -9,7 +9,6 @@ Ajuda Rebre Canvi de nom - Arxivar Còpia de seguretat Canvi de contrasenya @@ -94,7 +93,6 @@ Còpia de seguretat amb èxit Còpia de seguretat fallida! - Arxiu fallit! Canvi de nom fallit! Canvi de contrasenya fallit! La contrasenya ha canviat @@ -273,9 +271,8 @@ Torna\'m enrere! Detalls - Es farà una còpia de seguretat del portamonedes i s\'esborrarà. - Si, fes-ho! - No gràcies! + Si, fes-ho! + No gràcies! Crear un nou portamonedes Restablir portamonedes de només lectura @@ -420,4 +417,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3b90f30..851b765 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -9,7 +9,7 @@ Hilfe Empfangen Umbenennen - Archivieren + Löschen Backup Passwort ändern @@ -94,7 +94,7 @@ Backup erfolgreich Backup fehlgeschlagen! - Archivierung fehlgeschlagen! + Löschen fehlgeschlagen! Umbenennen fehlgeschlagen! Passwort ändern fehlgeschlagen! Passwort geändert @@ -266,9 +266,9 @@ Nein, doch nicht! Details - Das Wallet wird archiviert und dann gelöscht! - Ja, mach das! - Nein, danke! + Das Wallet wird gelöscht! + Ja, mach das! + Nein, danke! Neues Wallet erstellen View-Only-Wallet wiederherstellen @@ -422,4 +422,6 @@ Select a subaddress Long-press for details + Import wallet + Import failed! diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index ee14b0c..34c692a 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -9,7 +9,6 @@ Βοήθεια Λήψη Μετονομασία - Αρχειοθέτησε Δημιουργία αντίγραφου ασφαλείας Συνέχισε να πληκτρολογείς … @@ -92,7 +91,6 @@ Η δημιουργία αντίγραφου ασφαλείας ήταν επιτυχής Η δημιουργία αντίγραφου ασφαλείας απέτυχε! - Η αρχειοθέτηση απέτυχε! Η μετονομασία απέτυχε! Κόμβος(Δαίμονας) @@ -241,9 +239,8 @@ Πήγαινε με πίσω! Λεπτομέρειες - Το πορτοφόλι θα δημιουργήσει αντίγραφο ασφαλείας και στη συνέχεια θα διαγραφεί! - Ναι, κάνε αυτό! - Όχι ευχαριστώ! + Ναι, κάνε αυτό! + Όχι ευχαριστώ! Δημιουργία νέου πορτοφολιού Επαναφορά πορτοφολιού προβολής-μόνο @@ -422,4 +419,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index df99795..6237dcf 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -9,7 +9,6 @@ Helpo Ricevi Renomi - Arkivo Sekurkopio Ŝanĝi pasfrazon @@ -94,7 +93,6 @@ Sekurkopio sukcesis! Sekurkopio malsukcesis! - Arkivado malsukcesis! Renomado malsukcesis! Pasvortŝanĝo malsukcesis! Pasvortŝanĝo sukcesis! @@ -273,9 +271,8 @@ Malantaŭen! Detaloj - La monujo sekurkopiĝos kaj poste forviŝiĝos! - Jes, faru tion! - Ne dankon! + Jes, faru tion! + Ne dankon! Krei novan monujon Restaŭri nurvidan monujon @@ -422,4 +419,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 84f96d4..7023abe 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -9,7 +9,6 @@ Ayuda Recibir Renombrar - Archivar Copia de seguridad Cambiar contraseña @@ -49,7 +48,6 @@ Copia de seguridad exitosa ¡Copia de seguridad fallida! - ¡Archivado fallido! ¡Cambio de nombre fallido! ¡Cambio de contraseña fallido! Contraseña cambiada @@ -209,9 +207,9 @@ ¡Llévame de vuelta! Detalles - ¡El monedero será copiado y después borrado! - ¡Sí, hazlo! - ¡No, gracias! + ¡El monedero será borrado! + ¡Sí, hazlo! + ¡No, gracias! Crear nuevo monedero Restaurar monedero de sólo vista @@ -413,4 +411,9 @@ Elige una subdirección Presiona largo para ver detalles + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 02e5046..7f265e5 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -9,7 +9,6 @@ Abi Küsi raha Nimeta ümber - Arhiveeri Tagavarakoopia Vaheta parooli @@ -93,7 +92,6 @@ Teen ettevalmistusi …\nSee võib aega võtta! Tagavarakoopia ebaõnnestus! - Arhiveerimine ebaõnnestus! Ümber nimetamine ebaõnnestus! Parooli vahetamine ebaõnnestus! Parool vahetatud @@ -268,9 +266,8 @@ Vii mind tagasi! Lisainfo - Rahakotist tehakse tagavarakoopia ja siis see kustutatakse! - Täpselt nii! - Ei, tänan! + Täpselt nii! + Ei, tänan! Loo uus rahakott Taasta rahakott vaatamiseks @@ -420,4 +417,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7e94192..98d688b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,7 +9,6 @@ Aide Recevoir Renommer - Archiver Sauvegarder Modifier Phrase secrète @@ -95,7 +94,6 @@ Sauvegarde réussie Sauvegarde Échouée ! - Archivage Échouée ! Renommage Échoué ! Modification du mot de passe Échouée ! Modification du mot de passe réussie @@ -269,9 +267,9 @@ Non merci ! Détails - Le portefeuille sera sauvegardé puis supprimé ! - Oui, procéder ! - Non merci ! + Le portefeuille sera supprimé ! + Oui, procéder ! + Non merci ! Créer un nouveau portefeuille Restaurer un portefeuille d\'audit @@ -426,4 +424,9 @@ Select a subaddress Long-press for details + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index e9966d6..bcdbb14 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -9,7 +9,6 @@ Segítség Fogadás Átnevezés - Archiválás Biztonsági mentés Jelszómódosítás @@ -93,7 +92,6 @@ Műveletek befejezése…\nEz eltarthat egy ideig! Sikertelen biztonsági mentés! - Sikertelen archiválás! Sikertelen átnevezés! Sikertelen jelszómódosítás! Jelszó megváltoztatva @@ -266,9 +264,8 @@ Inkább ne! Részletek - A tárcáról biztonsági másolat készül, majd törlésre fog kerülni! - Igen, mehet! - Inkább ne! + Igen, mehet! + Inkább ne! Új tárca létrehozása Figyelőtárca visszaállítása @@ -424,4 +421,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 3ff7aa6..94b8b02 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -9,7 +9,6 @@ Aiuto Ricevi Rinomina - Archivia Backup Cambia passphrase @@ -95,7 +94,6 @@ Backup effettuato con successo Backup fallito! - Archiviazione fallita! Rinomina fallita! Modifica password fallita! Password cambiata @@ -268,9 +266,9 @@ Torna indietro! Dettagli - Il portafoglio verrà archiviato e poi eliminato! - Sì, procedi! - No grazie! + Il portafoglio verrà eliminato! + Sì, procedi! + No grazie! Crea un nuovo portafoglio Recupera un portafoglio solo-visualizzazione @@ -425,4 +423,9 @@ Select a subaddress Long-press for details + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ec6b209..d5721da 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -9,7 +9,6 @@ ヘルプ 受取り 名前の変更 - アーカイブ バックアップ パスフレーズの変更 @@ -93,7 +92,6 @@ 準備中 …\nしばらく時間がかかるかもしれません! バックアップに失敗しました! - アーカイブに失敗しました! 名前の変更に失敗しました! パスワード変更に失敗しました! パスワードが変更されました @@ -273,9 +271,8 @@ 戻ります! 詳細 - ウォレットはバックアップされてから削除されます! - はい、お願いします! - いいえ、結構です! + はい、お願いします! + いいえ、結構です! 新しいウォレットの作成 閲覧専用ウォレットを復元 @@ -425,4 +422,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 40bff1c..1b2125f 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -9,7 +9,6 @@ Hjelp Motta Gi nytt navn - Arkiver Backup [Forandre passord] @@ -93,7 +92,6 @@ Backup suksessfull Backup feila! - Arkivering feila! Kunne ikke gi nytt navn! [Passordforandring feila!] [Passord forandra] @@ -266,9 +264,8 @@ Ta meg tilbake! Detaljer - Lommeboka vil bli backa opp og så sletta! - Ja, gjør det! - Nei takk! + Ja, gjør det! + Nei takk! Lag ny lommebok Gjenoprett bare-se lommebok @@ -422,4 +419,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 119bfea..f99fa2a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -9,7 +9,6 @@ Help Ontvangen Hernoemen - Archiveren Back-up Wachtzin wijzigen @@ -93,7 +92,6 @@ Bezig met voltooien…\nDit kan even duren. Back-up mislukt! - Archiveren mislukt! Hernoemen mislukt! Wachtwoord wijzigen mislukt! Wachtwoord is gewijzigd @@ -263,9 +261,9 @@ Ga terug! Details - Er wordt een back-up gemaakt en daarna wordt de portemonnee verwijderd! - Ja, doe dat! - Nee, niet doen! + Er wordt de portemonnee verwijderd! + Ja, doe dat! + Nee, niet doen! Nieuwe portemonnee Alleen-lezen portemonnee herstellen @@ -422,4 +420,9 @@ Select a subaddress Long-press for details + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 4fb7b1e..d2af54f 100755 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -9,7 +9,6 @@ Ajuda Receber Renomear - Arquivar Backup Alterar Senha @@ -93,7 +92,6 @@ Finalizando o processo …\nPode demorar um pouco Erro ao criar backup! - Erro ao arquivar! Erro ao renomear! Erro ao alterar senha! Senha alterada @@ -265,9 +263,8 @@ Tire-me daqui! Detalhes - Farei um backup da carteira e depois vou deletá-la! - Sim, pode fazer! - Não, obrigado! + Sim, pode fazer! + Não, obrigado! Criar nova carteira Restaurar carteira \"Somente leitura\" @@ -414,4 +411,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 7fc18b4..c9e7046 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -9,7 +9,6 @@ Ajuda Receber Renomear - Arquivar Cópia de segurança Alterar palavra passe @@ -93,7 +92,6 @@ A concluir o processamento …\nIsto pode demorar! Cópia de segurança falhou! - Erro ao arquivar! Erro ao renomear! Erro ao alterar a palavra passe! Palavra passe alterada @@ -265,9 +263,8 @@ Volta atrás! Detalhes - Vai ser feita uma cópia de segurança da carteira e depois será apagada! - Sim, faz isso! - Não obrigado! + Sim, faz isso! + Não obrigado! Criar nova carteira Restaurar carteira apenas de visualização @@ -426,4 +423,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 334aa8f..54c92c2 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -9,7 +9,6 @@ Ajutor Primește Redenumește - Arhivă Backup Continuă … @@ -92,7 +91,6 @@ Copia de rezervă creată cu succes Copia de rezervă a eșuat! - Arhivarea a eșuat! Redenumirea a eșuat! Nodul @@ -241,9 +239,8 @@ Du-mă înapoi! Detalii - Voi face o copie de siguranță a portofelului apoi va fi șters! - Da, fă asta! - Nu mersi! + Da, fă asta! + Nu mersi! Creeaza portofel nou Restaurează portofel-vizualizare @@ -422,4 +419,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9b68e19..5d4bbff 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -9,7 +9,6 @@ Помощь Получить Переименовать - Архив Резервная копия Изменить пароль @@ -94,7 +93,6 @@ Для данного действия …\nМожет потребоваться некоторое время! Резервное копирование произошло с ошибкой! - Архивирование произошло с ошибкой! Переименование произошло с ошибкой! Изменение пароля произошло с ошибкой! Изменение пароля выполено успешно @@ -267,9 +265,9 @@ Верните меня обратно! Подробная информация - Этот кошелек будет скопирован, а затем удален! - Да, сделай это! - Нет, спасибо! + Этот кошелек будет удален! + Да, сделай это! + Нет, спасибо! Создать новый кошелек Восстановить кошелек только для просмотра @@ -426,4 +424,9 @@ Select a subaddress Long-press for details + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index b34b33b..c1895c2 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -9,7 +9,6 @@ Pomoc Prijať Premenovať - Archivovať Záloha Zmena Hesla @@ -94,7 +93,6 @@ Balím si veci …\nMôže to chvíľu trvať! Zálohovanie zlyhalo! - Archivácia zlyhala! Premenovanie zlyhalo! Zmena hesla zlyhala! Heslo zmenené @@ -264,9 +262,9 @@ Naspäť! Detaily - Peňaženka bude zálohovaná a následne zmazaná! - Áno, poďme na to! - Nie, díky! + Peňaženka bude zmazaná! + Áno, poďme na to! + Nie, díky! Vytvor novú peňaženku Obnoviť prezeraciu peňaženku @@ -423,4 +421,9 @@ Select a subaddress Long-press for details + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 5abd7da..7733d62 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -9,7 +9,6 @@ Pomoć Primi Preimenuj - Arhiva Bekap Promeni lozinku @@ -94,7 +93,6 @@ Bekap uspešan Bekap neuspeo! - Arhiviranje neuspelo! Preimenovanje neuspelo! Promena lozinke neuspela! Lozinka promenjena @@ -274,9 +272,9 @@ Vrati me nazad! Detalji - Novčanik će biti bekapovan a potom obrisan! - Da, uradi to! - Ne, hvala! + Novčanik će biti obrisan! + Da, uradi to! + Ne, hvala! Kreiraj novi novčanik Obnovi novčanik za pregled @@ -421,4 +419,9 @@ Select a subaddress Long-press for details + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 7d76007..ea05956 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -9,7 +9,6 @@ Hjälp Ta emot Byt namn - Arkivera Säkerhetskopiera Ändra lösenfras @@ -94,7 +93,6 @@ Säkerhetskopia sparad Det gick inte att säkerhetskopiera! - Det gick inte att arkivera! Det gick inte att byta namn! Det gick inte att ändra lösenord! Lösenordet har ändrats @@ -254,9 +252,8 @@ Ta mig tillbaka! Detaljer - Plånboken kommer att säkerhetskopieras och sedan tas bort! - Ja, gör det! - Nej tack! + Ja, gör det! + Nej tack! Skapa ny plånbok Återställ granskningsplånbok @@ -414,4 +411,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8f1de6b..df166ff 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -9,7 +9,6 @@ Допомога Отримати Перейменувати - Архів Резервна копія Змінити пароль @@ -94,7 +93,6 @@ Для даної дії …\nМоже знадобитися деякий час! Резервне копіювання відбулося з помилкою! - Архівування відбулося з помилкою! Перейменування відбулося з помилкою! Зміна паролю відбулася з помилкою! Зміну паролю виконано успішно @@ -267,9 +265,9 @@ Поверніть мене назад! Детальна інформація - Цей гаманець буде скопійовано, а потім знищено! - Так, зроби це! - Ні, дякую! + Цей гаманець буде знищено! + Так, зроби це! + Ні, дякую! Створити новий гаманець Відновити гаманець тільки для перегляду @@ -426,4 +424,9 @@ Select a subaddress Long-press for details + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4bbd57a..db38944 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -7,7 +7,6 @@ 帮助 收款 重命名 - 存档 备份 修改密码 强度不够哦 … @@ -70,7 +69,6 @@ 整合中…\n这将花上不少时间 备份成功 备份失败! - 存档失败! 重命名失败! 密码修改失败! 密码已修改 @@ -213,9 +211,8 @@ 安全 返回上一步 详情 - 即将开始备份钱包,完成后将删除钱包! - 好的,开始吧! - 不用了,谢谢! + 好的,开始吧! + 不用了,谢谢! 创建新钱包 恢复只读钱包 通过私钥恢复钱包 @@ -346,4 +343,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d3b717c..6f48545 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -9,7 +9,6 @@ 幫助 收款 重新命名 - 封存 備份 更改密碼 @@ -95,7 +94,6 @@ 備份成功 備份失敗! - 封存失敗! 重新命名失敗! 密碼更改失敗! 密碼更改成功 @@ -265,9 +263,8 @@ 不看了! 詳細資訊 - 錢包檔案將會被備份並且於列表中刪除! - 好的! - 不了! + 好的! + 不了! 建立新錢包 回復唯讀錢包 @@ -421,4 +418,10 @@ Select a subaddress Long-press for details + The wallet will be deleted! + Delete + Delete failed! + + Import wallet + Import failed! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index acc9f12..7da9eea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,7 +10,7 @@ Help Receive Rename - Archive + Delete Backup Change Passphrase @@ -86,16 +86,16 @@ Destination Address Notes - Backup in progress - Archive in progress - Rename in progress + Backup in progress + Archive in progress + Rename in progress Change Password in progress Wrapping things up …\nThis can take a while! Backup successful Backup failed! - Archive failed! + Delete failed! Rename failed! Change Password failed! Password changed @@ -132,7 +132,7 @@ Saved password is incorrect.\nPlease enter password manually. Wallet does not exist! Node must be set! - Wallet does not match selected net + Wallet does not match selected net (Watch Only) @@ -278,9 +278,9 @@ Take me back! Details - The wallet will be backed up and then deleted! - Yes, do that! - No thanks! + The wallet will be deleted! + Yes, do that! + No thanks! EUR @@ -495,4 +495,6 @@ Select a subaddress Long-press for details + Import wallet + Import failed!