/*
* Copyright 2011-2014 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 devcoin.wallet;
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.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.Application;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.StrictMode;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.widget.Toast;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.android.LogcatAppender;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import com.google.devcoin.core.Address;
import com.google.devcoin.core.ECKey;
import com.google.devcoin.core.Transaction;
import com.google.devcoin.core.Wallet;
import com.google.devcoin.store.UnreadableWalletException;
import com.google.devcoin.store.WalletProtobufSerializer;
import com.google.devcoin.utils.Threading;
import com.google.devcoin.wallet.WalletFiles;
import devcoin.wallet.service.BlockchainService;
import devcoin.wallet.service.BlockchainServiceImpl;
import devcoin.wallet.util.CrashReporter;
import devcoin.wallet.util.Io;
import devcoin.wallet.util.LinuxSecureRandom;
import devcoin.wallet.util.WalletUtils;
import devcoin.wallet.R;
/**
* @author Andreas Schildbach
*/
public class WalletApplication extends Application
{
private SharedPreferences prefs;
private ActivityManager activityManager;
private Intent blockchainServiceIntent;
private Intent blockchainServiceCancelCoinsReceivedIntent;
private Intent blockchainServiceResetBlockchainIntent;
private File walletFile;
private Wallet wallet;
private PackageInfo packageInfo;
private static final int KEY_ROTATION_VERSION_CODE = 135;
private static final Logger log = LoggerFactory.getLogger(WalletApplication.class);
@Override
public void onCreate()
{
new LinuxSecureRandom(); // init proper random number generator
initLogging();
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().permitDiskReads().permitDiskWrites().penaltyLog().build());
Threading.throwOnLockCycles();
log.info("configuration: " + (Constants.TEST ? "test" : "prod") + ", " + Constants.NETWORK_PARAMETERS.getId());
super.onCreate();
try
{
packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
}
catch (final NameNotFoundException x)
{
throw new RuntimeException(x);
}
CrashReporter.init(getCacheDir());
Threading.uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler()
{
@Override
public void uncaughtException(final Thread thread, final Throwable throwable)
{
log.info("bitcoinj uncaught exception", throwable);
CrashReporter.saveBackgroundTrace(throwable, packageInfo);
}
};
prefs = PreferenceManager.getDefaultSharedPreferences(this);
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();
wallet.autosaveToFile(walletFile, 1, TimeUnit.SECONDS, new WalletAutosaveEventListener());
final int lastVersionCode = prefs.getInt(Constants.PREFS_KEY_LAST_VERSION, 0);
prefs.edit().putInt(Constants.PREFS_KEY_LAST_VERSION, packageInfo.versionCode).commit();
if (packageInfo.versionCode > lastVersionCode)
log.info("detected app upgrade: " + lastVersionCode + " -> " + packageInfo.versionCode);
else if (packageInfo.versionCode < lastVersionCode)
log.warn("detected app downgrade: " + lastVersionCode + " -> " + packageInfo.versionCode);
if (lastVersionCode > 0 && lastVersionCode < KEY_ROTATION_VERSION_CODE && packageInfo.versionCode >= KEY_ROTATION_VERSION_CODE)
{
log.info("detected version jump crossing key rotation");
wallet.setKeyRotationTime(System.currentTimeMillis() / 1000);
}
ensureKey();
}
private void initLogging()
{
final File logDir = getDir("log", Constants.TEST ? Context.MODE_WORLD_READABLE : MODE_PRIVATE);
final File logFile = new File(logDir, "wallet.log");
final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
final PatternLayoutEncoder filePattern = new PatternLayoutEncoder();
filePattern.setContext(context);
filePattern.setPattern("%d{HH:mm:ss.SSS} [%thread] %logger{0} - %msg%n");
filePattern.start();
final RollingFileAppender<ILoggingEvent> fileAppender = new RollingFileAppender<ILoggingEvent>();
fileAppender.setContext(context);
fileAppender.setFile(logFile.getAbsolutePath());
final TimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new TimeBasedRollingPolicy<ILoggingEvent>();
rollingPolicy.setContext(context);
rollingPolicy.setParent(fileAppender);
rollingPolicy.setFileNamePattern(logDir.getAbsolutePath() + "/wallet.%d.log.gz");
rollingPolicy.setMaxHistory(7);
rollingPolicy.start();
fileAppender.setEncoder(filePattern);
fileAppender.setRollingPolicy(rollingPolicy);
fileAppender.start();
final PatternLayoutEncoder logcatTagPattern = new PatternLayoutEncoder();
logcatTagPattern.setContext(context);
logcatTagPattern.setPattern("%logger{0}");
logcatTagPattern.start();
final PatternLayoutEncoder logcatPattern = new PatternLayoutEncoder();
logcatPattern.setContext(context);
logcatPattern.setPattern("[%thread] %msg%n");
logcatPattern.start();
final LogcatAppender logcatAppender = new LogcatAppender();
logcatAppender.setContext(context);
logcatAppender.setTagEncoder(logcatTagPattern);
logcatAppender.setEncoder(logcatPattern);
logcatAppender.start();
final ch.qos.logback.classic.Logger log = context.getLogger(Logger.ROOT_LOGGER_NAME);
log.addAppender(fileAppender);
log.addAppender(logcatAppender);
log.setLevel(Level.INFO);
}
private static final class WalletAutosaveEventListener implements WalletFiles.Listener
{
@Override
public void onBeforeAutoSave(final File file)
{
}
@Override
public void onAfterAutoSave(final File file)
{
// make wallets world accessible in test mode
if (Constants.TEST)
Io.chmod(file, 0777);
}
}
public Wallet getWallet()
{
return wallet;
}
private void migrateWalletToProtobuf()
{
final File oldWalletFile = getFileStreamPath(Constants.WALLET_FILENAME);
if (oldWalletFile.exists())
{
log.info("found wallet to migrate");
final long start = System.currentTimeMillis();
// read
wallet = restoreWalletFromBackup();
try
{
// write
protobufSerializeWallet(wallet);
// delete
oldWalletFile.delete();
log.info("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.info("wallet loaded from: '" + walletFile + "', took " + (System.currentTimeMillis() - start) + "ms");
}
catch (final FileNotFoundException x)
{
log.error("problem loading wallet", x);
Toast.makeText(WalletApplication.this, x.getClass().getName(), Toast.LENGTH_LONG).show();
wallet = restoreWalletFromBackup();
}
catch (final UnreadableWalletException x)
{
log.error("problem loading wallet", x);
Toast.makeText(WalletApplication.this, x.getClass().getName(), Toast.LENGTH_LONG).show();
wallet = restoreWalletFromBackup();
}
finally
{
if (walletStream != null)
{
try
{
walletStream.close();
}
catch (final IOException x)
{
// swallow
}
}
}
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
{
wallet = new Wallet(Constants.NETWORK_PARAMETERS);
log.info("new wallet created");
}
// 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.info("wallet restored from backup: '" + Constants.WALLET_KEY_BACKUP_BASE58 + "'");
return wallet;
}
catch (final IOException x)
{
throw new RuntimeException(x);
}
}
private static Wallet readKeys(@Nonnull final InputStream is) throws IOException
{
final BufferedReader in = new BufferedReader(new InputStreamReader(is, Constants.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;
}
private void ensureKey()
{
for (final ECKey key : wallet.getKeys())
if (!wallet.isKeyRotating(key))
return; // found
log.info("wallet has no usable key - creating");
addNewKeyToWallet();
}
public void addNewKeyToWallet()
{
wallet.addKey(new ECKey());
backupKeys();
prefs.edit().putBoolean(Constants.PREFS_KEY_REMIND_BACKUP, true).commit();
}
public void saveWallet()
{
try
{
protobufSerializeWallet(wallet);
}
catch (final IOException x)
{
throw new RuntimeException(x);
}
}
private void protobufSerializeWallet(@Nonnull final Wallet wallet) throws IOException
{
final long start = System.currentTimeMillis();
wallet.saveToFile(walletFile);
// make wallets world accessible in test mode
if (Constants.TEST)
Io.chmod(walletFile, 0777);
log.debug("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)
{
log.error("problem writing key backup", x);
}
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)
{
log.error("problem writing key backup", x);
}
}
private void writeKeys(@Nonnull final OutputStream os) throws IOException
{
final List<ECKey> keys = new LinkedList<ECKey>();
for (final ECKey key : wallet.getKeys())
if (!wallet.isKeyRotating(key))
keys.add(key);
final Writer out = new OutputStreamWriter(os, Constants.UTF_8);
WalletUtils.writeKeys(out, keys);
out.close();
}
public Address determineSelectedAddress()
{
final String selectedAddress = prefs.getString(Constants.PREFS_KEY_SELECTED_ADDRESS, null);
Address firstAddress = null;
for (final ECKey key : wallet.getKeys())
{
if (!wallet.isKeyRotating(key))
{
final Address address = key.toAddress(Constants.NETWORK_PARAMETERS);
if (address.toString().equals(selectedAddress))
return address;
if (firstAddress == null)
firstAddress = address;
}
}
return firstAddress;
}
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
startService(blockchainServiceResetBlockchainIntent);
}
public void broadcastTransaction(@Nonnull final Transaction tx)
{
final Intent intent = new Intent(BlockchainService.ACTION_BROADCAST_TRANSACTION, null, this, BlockchainServiceImpl.class);
intent.putExtra(BlockchainService.ACTION_BROADCAST_TRANSACTION_HASH, tx.getHash().getBytes());
startService(intent);
}
public boolean isServiceRunning(final Class<? extends Service> serviceClass)
{
final String packageName = getPackageName();
for (final RunningServiceInfo serviceInfo : activityManager.getRunningServices(Integer.MAX_VALUE))
if (packageName.equals(serviceInfo.service.getPackageName()) && serviceClass.getName().equals(serviceInfo.service.getClassName()))
return true;
return false;
}
public PackageInfo packageInfo()
{
return packageInfo;
}
public final String applicationPackageFlavor()
{
final String packageName = getPackageName();
final int index = packageName.lastIndexOf('_');
if (index != -1)
return packageName.substring(index + 1);
else
return null;
}
public int maxConnectedPeers()
{
final int memoryClass = activityManager.getMemoryClass();
if (memoryClass <= Constants.MEMORY_CLASS_LOWEND)
return 4;
else
return 6;
}
}