Java tutorial
/* * 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 com.matthewmitchell.peercoin_android_wallet; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import com.matthewmitchell.peercoinj.core.Transaction; import com.matthewmitchell.peercoinj.core.VerificationException; import com.matthewmitchell.peercoinj.core.VersionMessage; import com.matthewmitchell.peercoinj.core.Wallet; import com.matthewmitchell.peercoinj.crypto.MnemonicCode; import com.matthewmitchell.peercoinj.store.UnreadableWalletException; import com.matthewmitchell.peercoinj.store.WalletProtobufSerializer; import com.matthewmitchell.peercoinj.utils.Threading; import com.matthewmitchell.peercoinj.wallet.Protos; import com.matthewmitchell.peercoinj.wallet.WalletFiles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.Application; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Handler; import android.os.StrictMode; import android.preference.PreferenceManager; import android.support.v4.content.LocalBroadcastManager; 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.matthewmitchell.peercoin_android_wallet.service.BlockchainService; import com.matthewmitchell.peercoin_android_wallet.service.BlockchainServiceImpl; import com.matthewmitchell.peercoin_android_wallet.util.CrashReporter; import com.matthewmitchell.peercoin_android_wallet.util.Io; import com.matthewmitchell.peercoin_android_wallet.util.LinuxSecureRandom; import com.matthewmitchell.peercoin_android_wallet.R; import static junit.framework.Assert.assertTrue; /** * @author Andreas Schildbach */ public class WalletApplication extends Application { private Configuration config = null; private ActivityManager activityManager; private Intent blockchainServiceIntent; private Intent blockchainServiceCancelCoinsReceivedIntent; private Intent blockchainServiceResetBlockchainIntent; private File walletFile; private Wallet wallet; private PackageInfo packageInfo; public static final String ACTION_WALLET_CHANGED = WalletApplication.class.getPackage().getName() + ".wallet_changed"; private static final Logger log = LoggerFactory.getLogger(WalletApplication.class); private ArrayList<Runnable> loadedCallbacks = new ArrayList<Runnable>(); private boolean isLoaded = false; private void initWallet() { new LinuxSecureRandom(); // init proper random number generator initLogging(); Threading.throwOnLockCycles(); log.info("=== starting app using configuration: {}, {}", Constants.TEST ? "test" : "prod", Constants.NETWORK_PARAMETERS.getId()); super.onCreate(); CrashReporter.init(getCacheDir()); Threading.uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(final Thread thread, final Throwable throwable) { log.info("peercoinj uncaught exception", throwable); CrashReporter.saveBackgroundTrace(throwable, packageInfo); } }; initMnemonicCode(); config = new Configuration(PreferenceManager.getDefaultSharedPreferences(this)); walletFile = getFileStreamPath(Constants.Files.WALLET_FILENAME_PROTOBUF); // Rename old wallets final File oldWalletFile = getFileStreamPath(Constants.Files.WALLET_FILENAME_PROTOBUF_OLD); if (oldWalletFile.exists()) oldWalletFile.renameTo(walletFile); loadWalletFromProtobuf(); config.updateLastVersionCode(packageInfo.versionCode); afterLoadWallet(); cleanupFiles(); synchronized (this) { isLoaded = true; for (Runnable callback : loadedCallbacks) callback.run(); } } public void setOnLoadedCallback(Runnable callback) { synchronized (this) { if (isLoaded) { callback.run(); return; } loadedCallbacks.add(callback); } } @Override public void onCreate() { // Do some stuff that is quick activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); packageInfo = packageInfoFromContext(this); 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); // Do not use main thread! new Thread(new Runnable() { @Override public void run() { initWallet(); } }).start(); } private void afterLoadWallet() { wallet.autosaveToFile(walletFile, 10, TimeUnit.SECONDS, new WalletAutosaveEventListener()); // clean up spam wallet.cleanup(); migrateBackup(); } 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 String BIP39_WORDLIST_FILENAME = "bip39-wordlist.txt"; private void initMnemonicCode() { try { final long start = System.currentTimeMillis(); MnemonicCode.INSTANCE = new MnemonicCode(getAssets().open(BIP39_WORDLIST_FILENAME), null); log.info("BIP39 wordlist loaded from: '" + BIP39_WORDLIST_FILENAME + "', took " + (System.currentTimeMillis() - start) + "ms"); } catch (final IOException x) { throw new Error(x); } } 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 Configuration getConfiguration() { return config; } public Wallet getWallet() { return wallet; } private void loadWalletFromProtobuf() { if (walletFile.exists()) { final long start = System.currentTimeMillis(); FileInputStream walletStream = null; try { walletStream = new FileInputStream(walletFile); wallet = new WalletProtobufSerializer().readWallet(walletStream); if (!wallet.getParams().equals(Constants.NETWORK_PARAMETERS)) throw new UnreadableWalletException( "bad wallet network parameters: " + wallet.getParams().getId()); 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); backupWallet(); config.armBackupReminder(); log.info("new wallet created"); } } private Wallet restoreWalletFromBackup() { InputStream is = null; try { is = openFileInput(Constants.Files.WALLET_KEY_BACKUP_PROTOBUF); final Wallet wallet = new WalletProtobufSerializer().readWallet(is); if (!wallet.isConsistent()) throw new Error("inconsistent backup"); resetBlockchain(); Toast.makeText(this, R.string.toast_wallet_reset, Toast.LENGTH_LONG).show(); log.info("wallet restored from backup: '" + Constants.Files.WALLET_KEY_BACKUP_PROTOBUF + "'"); return wallet; } catch (final IOException x) { throw new Error("cannot read backup", x); } catch (final UnreadableWalletException x) { throw new Error("cannot read backup", x); } finally { try { is.close(); } catch (final IOException x) { // swallow } } } 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"); } public void backupWallet() { final Protos.Wallet.Builder builder = new WalletProtobufSerializer().walletToProto(wallet).toBuilder(); // strip redundant builder.clearTransaction(); builder.clearLastSeenBlockHash(); builder.setLastSeenBlockHeight(-1); builder.clearLastSeenBlockTimeSecs(); final Protos.Wallet walletProto = builder.build(); OutputStream os = null; try { os = openFileOutput(Constants.Files.WALLET_KEY_BACKUP_PROTOBUF, Context.MODE_PRIVATE); walletProto.writeTo(os); } catch (final IOException x) { log.error("problem writing key backup", x); } finally { try { os.close(); } catch (final IOException x) { // swallow } } } private void migrateBackup() { if (!getFileStreamPath(Constants.Files.WALLET_KEY_BACKUP_PROTOBUF).exists()) { log.info("migrating automatic backup to protobuf"); // make sure there is at least one recent backup backupWallet(); } } private void cleanupFiles() { for (final String filename : fileList()) { if (filename.startsWith(Constants.Files.WALLET_KEY_BACKUP_BASE58) || filename.startsWith(Constants.Files.WALLET_KEY_BACKUP_PROTOBUF + '.') || filename.endsWith(".tmp")) { final File file = new File(getFilesDir(), filename); log.info("removing obsolete file: '{}'", file); file.delete(); } } } public void startBlockchainService(final boolean cancelCoinsReceived) { setOnLoadedCallback(new Runnable() { @Override public void run() { assertTrue(config != null); if (cancelCoinsReceived) startService(blockchainServiceCancelCoinsReceivedIntent); else startService(blockchainServiceIntent); } }); } public void stopBlockchainService() { stopService(blockchainServiceIntent); } public void resetBlockchain() { internalResetBlockchain(); final Intent broadcast = new Intent(ACTION_WALLET_CHANGED); broadcast.setPackage(getPackageName()); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } private void internalResetBlockchain() { // actually stops the service assertTrue(config != null); startService(blockchainServiceResetBlockchainIntent); } public void replaceWallet(final Wallet newWallet) { internalResetBlockchain(); // implicitly stops blockchain service wallet.shutdownAutosaveAndWait(); wallet = newWallet; config.maybeIncrementBestChainHeightEver(newWallet.getLastBlockSeenHeight()); afterLoadWallet(); final Intent broadcast = new Intent(ACTION_WALLET_CHANGED); broadcast.setPackage(getPackageName()); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); config.disarmBackupReminder(); } public void processDirectTransaction(@Nonnull final Transaction tx) throws VerificationException { if (wallet.isTransactionRelevant(tx)) { wallet.receivePending(tx, null); broadcastTransaction(tx); } } 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()); assertTrue(config != null); startService(intent); } public static PackageInfo packageInfoFromContext(final Context context) { try { return context.getPackageManager().getPackageInfo(context.getPackageName(), 0); } catch (final NameNotFoundException x) { throw new RuntimeException(x); } } 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 static String httpUserAgent(final String versionName) { final VersionMessage versionMessage = new VersionMessage(Constants.NETWORK_PARAMETERS, 0); versionMessage.appendToSubVer(Constants.USER_AGENT, versionName, null); return versionMessage.subVer; } public String httpUserAgent() { return httpUserAgent(packageInfo().versionName); } public int maxConnectedPeers() { final int memoryClass = activityManager.getMemoryClass(); if (memoryClass <= Constants.MEMORY_CLASS_LOWEND) return 4; else return 6; } public void scheduleStartBlockchainService() { final WalletApplication wa = this; this.setOnLoadedCallback(new Runnable() { @Override public void run() { final long lastUsedAgo = config.getLastUsedAgo(); // apply some backoff final long alarmInterval; if (lastUsedAgo < Constants.LAST_USAGE_THRESHOLD_JUST_MS) alarmInterval = AlarmManager.INTERVAL_FIFTEEN_MINUTES; else if (lastUsedAgo < Constants.LAST_USAGE_THRESHOLD_RECENTLY_MS) alarmInterval = AlarmManager.INTERVAL_HALF_DAY; else alarmInterval = AlarmManager.INTERVAL_DAY; log.info("last used {} minutes ago, rescheduling blockchain sync in roughly {} minutes", lastUsedAgo / DateUtils.MINUTE_IN_MILLIS, alarmInterval / DateUtils.MINUTE_IN_MILLIS); assertTrue(config != null); final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); final PendingIntent alarmIntent = PendingIntent.getService(wa, 0, new Intent(wa, BlockchainServiceImpl.class), 0); alarmManager.cancel(alarmIntent); // workaround for no inexact set() before KitKat final long now = System.currentTimeMillis(); alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, now + alarmInterval, AlarmManager.INTERVAL_DAY, alarmIntent); } }); } public boolean isLoaded() { // Wait for loadedCallbacks to run synchronized (this) { return isLoaded; } } }