/*
* Copyright 2011-2013 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.schildbach.wallet.digitalcoin;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.Toast;
import com.google.digitalcoin.core.Address;
import com.google.digitalcoin.core.ECKey;
import com.google.digitalcoin.core.Wallet;
import com.google.digitalcoin.core.Wallet.AutosaveEventListener;
import com.google.digitalcoin.store.WalletProtobufSerializer;
import de.schildbach.wallet.digitalcoin.service.BlockchainService;
import de.schildbach.wallet.digitalcoin.service.BlockchainServiceImpl;
import de.schildbach.wallet.digitalcoin.util.CrashReporter;
import de.schildbach.wallet.digitalcoin.util.StrictModeWrapper;
import de.schildbach.wallet.digitalcoin.util.WalletUtils;
import de.schildbach.wallet.digitalcoin.R;
/**
* @author Andreas Schildbach
*/
public class WalletApplication extends Application
{
private File walletFile;
private Wallet wallet;
private Intent blockchainServiceIntent;
private Intent blockchainServiceCancelCoinsReceivedIntent;
private Intent blockchainServiceResetBlockchainIntent;
private ActivityManager activityManager;
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final String TAG = "digitalcoin"+WalletApplication.class.getSimpleName();
@Override
public void onCreate()
{
try
{
StrictModeWrapper.init();
}
catch (final Error x)
{
Log.i(TAG, "StrictMode not available");
}
Log.d(TAG, ".onCreate()");
super.onCreate();
CrashReporter.init(getCacheDir());
activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
blockchainServiceIntent = new Intent(this, BlockchainServiceImpl.class);
blockchainServiceCancelCoinsReceivedIntent = new Intent(BlockchainService.ACTION_CANCEL_COINS_RECEIVED, null, this,
BlockchainServiceImpl.class);
blockchainServiceResetBlockchainIntent = new Intent(BlockchainService.ACTION_RESET_BLOCKCHAIN, null, this, BlockchainServiceImpl.class);
walletFile = getFileStreamPath(Constants.WALLET_FILENAME_PROTOBUF);
migrateWalletToProtobuf();
loadWalletFromProtobuf();
backupKeys();
wallet.autosaveToFile(walletFile, 1, TimeUnit.SECONDS, new WalletAutosaveEventListener());
}
private static final class WalletAutosaveEventListener implements AutosaveEventListener
{
public boolean caughtException(final Throwable t)
{
throw new Error(t);
}
public void onBeforeAutoSave(final File file)
{
}
public void onAfterAutoSave(final File file)
{
// make wallets world accessible in test mode
if (Constants.TEST)
WalletUtils.chmod(file, 0777);
}
}
public Wallet getWallet()
{
return wallet;
}
private void migrateWalletToProtobuf()
{
final File oldWalletFile = getFileStreamPath(Constants.WALLET_FILENAME);
if (oldWalletFile.exists())
{
Log.i(TAG, "found wallet to migrate");
final long start = System.currentTimeMillis();
// read
wallet = restoreWalletFromBackup();
try
{
// write
protobufSerializeWallet(wallet);
// delete
oldWalletFile.delete();
Log.i(TAG, "wallet migrated: '" + oldWalletFile + "', took " + (System.currentTimeMillis() - start) + "ms");
}
catch (final IOException x)
{
throw new Error("cannot migrate wallet", x);
}
}
}
private void loadWalletFromProtobuf()
{
if (walletFile.exists())
{
final long start = System.currentTimeMillis();
FileInputStream walletStream = null;
try
{
walletStream = new FileInputStream(walletFile);
wallet = new WalletProtobufSerializer().readWallet(walletStream);
Log.i(TAG, "wallet loaded from: '" + walletFile + "', took " + (System.currentTimeMillis() - start) + "ms");
}
catch (final IOException x)
{
x.printStackTrace();
Toast.makeText(WalletApplication.this, x.getClass().getName(), Toast.LENGTH_LONG).show();
wallet = restoreWalletFromBackup();
}
catch (final IllegalStateException x)
{
x.printStackTrace();
Toast.makeText(WalletApplication.this, x.getClass().getName(), Toast.LENGTH_LONG).show();
wallet = restoreWalletFromBackup();
}
finally
{
if (walletStream != null)
{
try
{
walletStream.close();
}
catch (final IOException x)
{
x.printStackTrace();
}
}
}
if (!wallet.isConsistent())
{
Toast.makeText(this, "inconsistent wallet: " + walletFile, Toast.LENGTH_LONG).show();
wallet = restoreWalletFromBackup();
}
if (!wallet.getParams().equals(Constants.NETWORK_PARAMETERS))
throw new Error("bad wallet network parameters: " + wallet.getParams().getId());
}
else
{
try
{
wallet = restoreWalletFromSnapshot();
}
catch (final FileNotFoundException x)
{
wallet = new Wallet(Constants.NETWORK_PARAMETERS);
wallet.addKey(new ECKey());
try
{
protobufSerializeWallet(wallet);
Log.i(TAG, "wallet created: '" + walletFile + "'");
}
catch (final IOException x2)
{
throw new Error("wallet cannot be created", x2);
}
}
}
// this check is needed so encrypted wallets won't get their private keys removed accidently
for (final ECKey key : wallet.getKeys())
if (key.getPrivKeyBytes() == null)
throw new Error("found read-only key, but wallet is likely an encrypted wallet from the future");
}
private Wallet restoreWalletFromBackup()
{
try
{
final Wallet wallet = readKeys(openFileInput(Constants.WALLET_KEY_BACKUP_BASE58));
resetBlockchain();
Toast.makeText(this, R.string.toast_wallet_reset, Toast.LENGTH_LONG).show();
Log.i(TAG, "wallet restored from backup: '" + Constants.WALLET_KEY_BACKUP_BASE58 + "'");
return wallet;
}
catch (final IOException x)
{
throw new RuntimeException(x);
}
}
private Wallet restoreWalletFromSnapshot() throws FileNotFoundException
{
try
{
final Wallet wallet = readKeys(getAssets().open(Constants.WALLET_KEY_BACKUP_SNAPSHOT));
Log.i(TAG, "wallet restored from snapshot: '" + Constants.WALLET_KEY_BACKUP_SNAPSHOT + "'");
return wallet;
}
catch (final FileNotFoundException x)
{
throw x;
}
catch (final IOException x)
{
throw new RuntimeException(x);
}
}
private static Wallet readKeys(final InputStream is) throws IOException
{
final BufferedReader in = new BufferedReader(new InputStreamReader(is, UTF_8));
final List<ECKey> keys = WalletUtils.readKeys(in);
in.close();
final Wallet wallet = new Wallet(Constants.NETWORK_PARAMETERS);
for (final ECKey key : keys)
wallet.addKey(key);
return wallet;
}
public void addNewKeyToWallet()
{
wallet.addKey(new ECKey());
backupKeys();
}
public void saveWallet()
{
try
{
protobufSerializeWallet(wallet);
}
catch (final IOException x)
{
throw new RuntimeException(x);
}
}
private void protobufSerializeWallet(final Wallet wallet) throws IOException
{
final long start = System.currentTimeMillis();
wallet.saveToFile(walletFile);
// make wallets world accessible in test mode
if (Constants.TEST)
WalletUtils.chmod(walletFile, 0777);
Log.d(TAG, "wallet saved to: '" + walletFile + "', took " + (System.currentTimeMillis() - start) + "ms");
}
private void backupKeys()
{
try
{
writeKeys(openFileOutput(Constants.WALLET_KEY_BACKUP_BASE58, Context.MODE_PRIVATE));
}
catch (final IOException x)
{
x.printStackTrace();
}
try
{
final String filename = String.format(Locale.US, "%s.%02d", Constants.WALLET_KEY_BACKUP_BASE58,
(System.currentTimeMillis() / DateUtils.DAY_IN_MILLIS) % 100l);
writeKeys(openFileOutput(filename, Context.MODE_PRIVATE));
}
catch (final IOException x)
{
x.printStackTrace();
}
}
private void writeKeys(final OutputStream os) throws IOException
{
final Writer out = new OutputStreamWriter(os, UTF_8);
WalletUtils.writeKeys(out, wallet.keychain);
out.close();
}
public Address determineSelectedAddress()
{
final ArrayList<ECKey> keychain = wallet.keychain;
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
final String selectedAddress = prefs.getString(Constants.PREFS_KEY_SELECTED_ADDRESS, null);
if (selectedAddress != null)
{
for (final ECKey key : keychain)
{
final Address address = key.toAddress(Constants.NETWORK_PARAMETERS);
if (address.toString().equals(selectedAddress))
return address;
}
}
return keychain.get(0).toAddress(Constants.NETWORK_PARAMETERS);
}
public void startBlockchainService(final boolean cancelCoinsReceived)
{
if (cancelCoinsReceived)
startService(blockchainServiceCancelCoinsReceivedIntent);
else
startService(blockchainServiceIntent);
}
public void stopBlockchainService()
{
stopService(blockchainServiceIntent);
}
public void resetBlockchain()
{
// actually stops the service
Log.d("digitalcoin", "Sending blockchain service reset intent");
startService(blockchainServiceResetBlockchainIntent);
android.os.Process.killProcess(android.os.Process.myPid());
}
public final int applicationVersionCode()
{
try
{
return getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
}
catch (NameNotFoundException x)
{
return 0;
}
}
public final String applicationVersionName()
{
try
{
return getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
}
catch (NameNotFoundException x)
{
return "unknown";
}
}
public int maxConnectedPeers()
{
final int memoryClass = activityManager.getMemoryClass();
if (memoryClass <= 32)
return 4;
else
return 6;
}
}