Java tutorial
/* * Copyright 2011-2015 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.service; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nullable; import org.bitcoinj.core.Address; import org.bitcoinj.core.Block; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.CheckpointManager; import org.bitcoinj.core.Coin; import org.bitcoinj.core.FilteredBlock; import org.bitcoinj.core.Peer; import org.bitcoinj.core.PeerGroup; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence.ConfidenceType; import org.bitcoinj.core.listeners.AbstractPeerDataEventListener; import org.bitcoinj.core.listeners.PeerConnectedEventListener; import org.bitcoinj.core.listeners.PeerDataEventListener; import org.bitcoinj.core.listeners.PeerDisconnectedEventListener; import org.bitcoinj.net.discovery.MultiplexingDiscovery; import org.bitcoinj.net.discovery.PeerDiscovery; import org.bitcoinj.net.discovery.PeerDiscoveryException; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.SPVBlockStore; import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener; import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Stopwatch; import de.schildbach.wallet.Configuration; import de.schildbach.wallet.Constants; import de.schildbach.wallet.R; import de.schildbach.wallet.WalletApplication; import de.schildbach.wallet.WalletBalanceWidgetProvider; import de.schildbach.wallet.data.AddressBookProvider; import de.schildbach.wallet.data.ExchangeRate; import de.schildbach.wallet.data.ExchangeRateLiveData; import de.schildbach.wallet.data.TimeLiveData; import de.schildbach.wallet.data.WalletBalanceLiveData; import de.schildbach.wallet.data.WalletLiveData; import de.schildbach.wallet.service.BlockchainState.Impediment; import de.schildbach.wallet.ui.WalletActivity; import de.schildbach.wallet.util.CrashReporter; import de.schildbach.wallet.util.WalletUtils; import android.app.AlarmManager; import android.app.NotificationManager; import android.app.PendingIntent; import android.arch.lifecycle.LifecycleService; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.Observer; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; import android.text.format.DateUtils; /** * @author Andreas Schildbach */ public class BlockchainService extends LifecycleService { private WalletApplication application; private Configuration config; private WalletLiveData wallet; private BlockStore blockStore; private File blockChainFile; private BlockChain blockChain; @Nullable private PeerGroup peerGroup; private final Handler handler = new Handler(); private final Handler delayHandler = new Handler(); private WakeLock wakeLock; private PeerConnectivityListener peerConnectivityListener; private NotificationManager nm; private ImpedimentsLiveData impediments; private int notificationCount = 0; private Coin notificationAccumulatedAmount = Coin.ZERO; private final List<Address> notificationAddresses = new LinkedList<Address>(); private AtomicInteger transactionsReceived = new AtomicInteger(); private long serviceCreatedAt; private boolean resetBlockchainOnShutdown = false; private static final int MIN_COLLECT_HISTORY = 2; private static final int IDLE_BLOCK_TIMEOUT_MIN = 2; private static final int IDLE_TRANSACTION_TIMEOUT_MIN = 9; private static final int MAX_HISTORY_SIZE = Math.max(IDLE_TRANSACTION_TIMEOUT_MIN, IDLE_BLOCK_TIMEOUT_MIN); private static final long BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS = DateUtils.SECOND_IN_MILLIS; public static final String ACTION_PEER_STATE = BlockchainService.class.getPackage().getName() + ".peer_state"; public static final String ACTION_PEER_STATE_NUM_PEERS = "num_peers"; public static final String ACTION_BLOCKCHAIN_STATE = BlockchainService.class.getPackage().getName() + ".blockchain_state"; private static final String ACTION_CANCEL_COINS_RECEIVED = BlockchainService.class.getPackage().getName() + ".cancel_coins_received"; private static final String ACTION_RESET_BLOCKCHAIN = BlockchainService.class.getPackage().getName() + ".reset_blockchain"; private static final String ACTION_BROADCAST_TRANSACTION = BlockchainService.class.getPackage().getName() + ".broadcast_transaction"; private static final String ACTION_BROADCAST_TRANSACTION_HASH = "hash"; private static final Logger log = LoggerFactory.getLogger(BlockchainService.class); public static void start(final Context context, final boolean cancelCoinsReceived) { if (cancelCoinsReceived) context.startService(new Intent(BlockchainService.ACTION_CANCEL_COINS_RECEIVED, null, context, BlockchainService.class)); else context.startService(new Intent(context, BlockchainService.class)); } public static void stop(final Context context) { context.stopService(new Intent(context, BlockchainService.class)); } public static void scheduleStart(final WalletApplication application) { final Configuration config = application.getConfiguration(); 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); final AlarmManager alarmManager = (AlarmManager) application.getSystemService(Context.ALARM_SERVICE); final PendingIntent alarmIntent = PendingIntent.getService(application, 0, new Intent(application, BlockchainService.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 static void resetBlockchain(final Context context) { // implicitly stops blockchain service context.startService( new Intent(BlockchainService.ACTION_RESET_BLOCKCHAIN, null, context, BlockchainService.class)); } public static void broadcastTransaction(final Context context, final Transaction tx) { final Intent intent = new Intent(BlockchainService.ACTION_BROADCAST_TRANSACTION, null, context, BlockchainService.class); intent.putExtra(BlockchainService.ACTION_BROADCAST_TRANSACTION_HASH, tx.getHash().getBytes()); context.startService(intent); } private static class NewTransactionLiveData extends LiveData<Transaction> { private final Wallet wallet; public NewTransactionLiveData(final Wallet wallet) { this.wallet = wallet; } @Override protected void onActive() { wallet.addCoinsReceivedEventListener(Threading.SAME_THREAD, walletListener); wallet.addCoinsSentEventListener(Threading.SAME_THREAD, walletListener); } @Override protected void onInactive() { wallet.removeCoinsSentEventListener(walletListener); wallet.removeCoinsReceivedEventListener(walletListener); } private final WalletListener walletListener = new WalletListener(); private class WalletListener implements WalletCoinsReceivedEventListener, WalletCoinsSentEventListener { @Override public void onCoinsReceived(final Wallet wallet, final Transaction tx, final Coin prevBalance, final Coin newBalance) { postValue(tx); } @Override public void onCoinsSent(final Wallet wallet, final Transaction tx, final Coin prevBalance, final Coin newBalance) { postValue(tx); } } } private void notifyCoinsReceived(@Nullable final Address address, final Coin amount, final Sha256Hash transactionHash) { notificationCount++; notificationAccumulatedAmount = notificationAccumulatedAmount.add(amount); if (address != null && !notificationAddresses.contains(address)) notificationAddresses.add(address); final MonetaryFormat btcFormat = config.getFormat(); final String packageFlavor = application.applicationPackageFlavor(); final String msgSuffix = packageFlavor != null ? " [" + packageFlavor + "]" : ""; // summary notification final NotificationCompat.Builder summaryNotification = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ID_RECEIVED); summaryNotification.setGroup(Constants.NOTIFICATION_GROUP_KEY_RECEIVED); summaryNotification.setGroupSummary(true); summaryNotification.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); summaryNotification.setWhen(System.currentTimeMillis()); summaryNotification.setSmallIcon(R.drawable.stat_notify_received_24dp); summaryNotification.setContentTitle( getString(R.string.notification_coins_received_msg, btcFormat.format(notificationAccumulatedAmount)) + msgSuffix); if (!notificationAddresses.isEmpty()) { final StringBuilder text = new StringBuilder(); for (final Address notificationAddress : notificationAddresses) { if (text.length() > 0) text.append(", "); final String addressStr = notificationAddress.toBase58(); final String label = AddressBookProvider.resolveLabel(getApplicationContext(), addressStr); text.append(label != null ? label : addressStr); } summaryNotification.setContentText(text); } summaryNotification .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, WalletActivity.class), 0)); nm.notify(Constants.NOTIFICATION_ID_COINS_RECEIVED, summaryNotification.build()); // child notification final NotificationCompat.Builder childNotification = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ID_RECEIVED); childNotification.setGroup(Constants.NOTIFICATION_GROUP_KEY_RECEIVED); childNotification.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); childNotification.setWhen(System.currentTimeMillis()); childNotification.setSmallIcon(R.drawable.stat_notify_received_24dp); final String msg = getString(R.string.notification_coins_received_msg, btcFormat.format(amount)) + msgSuffix; childNotification.setTicker(msg); childNotification.setContentTitle(msg); if (address != null) { final String addressStr = address.toBase58(); final String addressLabel = AddressBookProvider.resolveLabel(getApplicationContext(), addressStr); if (addressLabel != null) childNotification.setContentText(addressLabel); else childNotification.setContentText(addressStr); } childNotification .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, WalletActivity.class), 0)); childNotification .setSound(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.coins_received)); nm.notify(transactionHash.toString(), Constants.NOTIFICATION_ID_COINS_RECEIVED, childNotification.build()); } private final class PeerConnectivityListener implements PeerConnectedEventListener, PeerDisconnectedEventListener, OnSharedPreferenceChangeListener { private int peerCount; private AtomicBoolean stopped = new AtomicBoolean(false); public PeerConnectivityListener() { config.registerOnSharedPreferenceChangeListener(this); } public void stop() { stopped.set(true); config.unregisterOnSharedPreferenceChangeListener(this); nm.cancel(Constants.NOTIFICATION_ID_CONNECTED); } @Override public void onPeerConnected(final Peer peer, final int peerCount) { this.peerCount = peerCount; changed(peerCount); } @Override public void onPeerDisconnected(final Peer peer, final int peerCount) { this.peerCount = peerCount; changed(peerCount); } @Override public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { if (Configuration.PREFS_KEY_CONNECTIVITY_NOTIFICATION.equals(key)) changed(peerCount); } private void changed(final int numPeers) { if (stopped.get()) return; handler.post(new Runnable() { @Override public void run() { final boolean connectivityNotificationEnabled = config.getConnectivityNotificationEnabled(); if (!connectivityNotificationEnabled || numPeers == 0) { stopForeground(true); } else { final NotificationCompat.Builder notification = new NotificationCompat.Builder( BlockchainService.this, Constants.NOTIFICATION_CHANNEL_ID_ONGOING); notification.setSmallIcon(R.drawable.stat_notify_peers, Math.min(numPeers, 4)); notification.setContentTitle(getString(R.string.app_name)); notification.setContentText(getString(R.string.notification_peers_connected_msg, numPeers)); notification.setContentIntent(PendingIntent.getActivity(BlockchainService.this, 0, new Intent(BlockchainService.this, WalletActivity.class), 0)); notification.setWhen(System.currentTimeMillis()); notification.setOngoing(true); startForeground(Constants.NOTIFICATION_ID_CONNECTED, notification.build()); } // send broadcast broadcastPeerState(numPeers); } }); } } private final PeerDataEventListener blockchainDownloadListener = new AbstractPeerDataEventListener() { private final AtomicLong lastMessageTime = new AtomicLong(0); @Override public void onBlocksDownloaded(final Peer peer, final Block block, final FilteredBlock filteredBlock, final int blocksLeft) { delayHandler.removeCallbacksAndMessages(null); final long now = System.currentTimeMillis(); if (now - lastMessageTime.get() > BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS) delayHandler.post(runnable); else delayHandler.postDelayed(runnable, BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS); } private final Runnable runnable = new Runnable() { @Override public void run() { lastMessageTime.set(System.currentTimeMillis()); config.maybeIncrementBestChainHeightEver(blockChain.getChainHead().getHeight()); broadcastBlockchainState(); } }; }; private static class ImpedimentsLiveData extends LiveData<Set<Impediment>> { private final WalletApplication application; private final ConnectivityManager connectivityManager; private final Set<Impediment> impediments = EnumSet.noneOf(Impediment.class); public ImpedimentsLiveData(final WalletApplication application) { this.application = application; this.connectivityManager = (ConnectivityManager) application .getSystemService(Context.CONNECTIVITY_SERVICE); setValue(impediments); } @Override protected void onActive() { final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); // implicitly start PeerGroup final Intent intent = application.registerReceiver(connectivityReceiver, intentFilter); if (intent != null) handleIntent(intent); } @Override protected void onInactive() { application.unregisterReceiver(connectivityReceiver); } private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { handleIntent(intent); } }; private void handleIntent(final Intent intent) { final String action = intent.getAction(); if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { final NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); final boolean hasConnectivity = networkInfo != null && networkInfo.isConnected(); if (hasConnectivity) impediments.remove(Impediment.NETWORK); else impediments.add(Impediment.NETWORK); if (log.isInfoEnabled()) { final StringBuilder s = new StringBuilder("active network is ") .append(hasConnectivity ? "up" : "down"); if (networkInfo != null) { s.append(", type: ").append(networkInfo.getTypeName()); s.append(", state: ").append(networkInfo.getState()).append('/') .append(networkInfo.getDetailedState()); final String extraInfo = networkInfo.getExtraInfo(); if (extraInfo != null) s.append(", extraInfo: ").append(extraInfo); final String reason = networkInfo.getReason(); if (reason != null) s.append(", reason: ").append(reason); } log.info(s.toString()); } } else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { impediments.add(Impediment.STORAGE); log.info("device storage low"); } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { impediments.remove(Impediment.STORAGE); log.info("device storage ok"); } setValue(impediments); } } public class LocalBinder extends Binder { public BlockchainService getService() { return BlockchainService.this; } } private final IBinder mBinder = new LocalBinder(); @Override public IBinder onBind(final Intent intent) { log.debug(".onBind()"); return mBinder; } @Override public boolean onUnbind(final Intent intent) { log.debug(".onUnbind()"); return super.onUnbind(intent); } @Override public void onCreate() { serviceCreatedAt = System.currentTimeMillis(); log.debug(".onCreate()"); super.onCreate(); nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); application = (WalletApplication) getApplication(); config = application.getConfiguration(); blockChainFile = new File(getDir("blockstore", Context.MODE_PRIVATE), Constants.Files.BLOCKCHAIN_FILENAME); peerConnectivityListener = new PeerConnectivityListener(); broadcastPeerState(0); final WalletBalanceLiveData walletBalance = new WalletBalanceLiveData(application); final ExchangeRateLiveData exchangeRate = new ExchangeRateLiveData(application); walletBalance.observe(this, new Observer<Coin>() { @Override public void onChanged(final Coin walletBalance) { WalletBalanceWidgetProvider.updateWidgets(BlockchainService.this, walletBalance, exchangeRate.getValue()); } }); if (Constants.ENABLE_EXCHANGE_RATES) { exchangeRate.observe(this, new Observer<ExchangeRate>() { @Override public void onChanged(final ExchangeRate exchangeRate) { final Coin balance = walletBalance.getValue(); if (balance != null) WalletBalanceWidgetProvider.updateWidgets(BlockchainService.this, balance, exchangeRate); } }); } wallet = new WalletLiveData(application); wallet.observe(this, new Observer<Wallet>() { @Override public void onChanged(final Wallet wallet) { BlockchainService.this.wallet.removeObserver(this); final boolean blockChainFileExists = blockChainFile.exists(); if (!blockChainFileExists) { log.info("blockchain does not exist, resetting wallet"); wallet.reset(); } try { blockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); blockStore.getChainHead(); // detect corruptions as early as possible final long earliestKeyCreationTime = wallet.getEarliestKeyCreationTime(); if (!blockChainFileExists && earliestKeyCreationTime > 0) { try { final Stopwatch watch = Stopwatch.createStarted(); final InputStream checkpointsInputStream = getAssets() .open(Constants.Files.CHECKPOINTS_FILENAME); CheckpointManager.checkpoint(Constants.NETWORK_PARAMETERS, checkpointsInputStream, blockStore, earliestKeyCreationTime); watch.stop(); log.info("checkpoints loaded from '{}', took {}", Constants.Files.CHECKPOINTS_FILENAME, watch); } catch (final IOException x) { log.error("problem reading checkpoints, continuing without", x); } } } catch (final BlockStoreException x) { blockChainFile.delete(); final String msg = "blockstore cannot be created"; log.error(msg, x); throw new Error(msg, x); } try { blockChain = new BlockChain(Constants.NETWORK_PARAMETERS, wallet, blockStore); } catch (final BlockStoreException x) { throw new Error("blockchain cannot be created", x); } observeLiveDatasThatAreDependentOnWalletAndBlockchain(); } }); } private void observeLiveDatasThatAreDependentOnWalletAndBlockchain() { final NewTransactionLiveData newTransaction = new NewTransactionLiveData(wallet.getValue()); newTransaction.observe(this, new Observer<Transaction>() { @Override public void onChanged(final Transaction tx) { final Wallet wallet = BlockchainService.this.wallet.getValue(); transactionsReceived.incrementAndGet(); final Coin amount = tx.getValue(wallet); if (amount.isPositive()) { final Address address = WalletUtils.getWalletAddressOfReceived(tx, wallet); final ConfidenceType confidenceType = tx.getConfidence().getConfidenceType(); final Sha256Hash hash = tx.getHash(); final boolean replaying = blockChain.getBestChainHeight() < config.getBestChainHeightEver(); final boolean isReplayedTx = confidenceType == ConfidenceType.BUILDING && replaying; if (!isReplayedTx) notifyCoinsReceived(address, amount, hash); } } }); final TimeLiveData time = new TimeLiveData(application); time.observe(this, new Observer<Date>() { private int lastChainHeight = 0; private final List<ActivityHistoryEntry> activityHistory = new LinkedList<ActivityHistoryEntry>(); @Override public void onChanged(final Date time) { final int chainHeight = blockChain.getBestChainHeight(); if (lastChainHeight > 0) { final int numBlocksDownloaded = chainHeight - lastChainHeight; final int numTransactionsReceived = transactionsReceived.getAndSet(0); // push history activityHistory.add(0, new ActivityHistoryEntry(numTransactionsReceived, numBlocksDownloaded)); // trim while (activityHistory.size() > MAX_HISTORY_SIZE) activityHistory.remove(activityHistory.size() - 1); // print final StringBuilder builder = new StringBuilder(); for (final ActivityHistoryEntry entry : activityHistory) { if (builder.length() > 0) builder.append(", "); builder.append(entry); } log.info("History of transactions/blocks: " + builder); // determine if block and transaction activity is idling boolean isIdle = false; if (activityHistory.size() >= MIN_COLLECT_HISTORY) { isIdle = true; for (int i = 0; i < activityHistory.size(); i++) { final ActivityHistoryEntry entry = activityHistory.get(i); final boolean blocksActive = entry.numBlocksDownloaded > 0 && i <= IDLE_BLOCK_TIMEOUT_MIN; final boolean transactionsActive = entry.numTransactionsReceived > 0 && i <= IDLE_TRANSACTION_TIMEOUT_MIN; if (blocksActive || transactionsActive) { isIdle = false; break; } } } // if idling, shutdown service if (isIdle) { log.info("idling detected, stopping service"); stopSelf(); } } lastChainHeight = chainHeight; } final class ActivityHistoryEntry { public final int numTransactionsReceived; public final int numBlocksDownloaded; public ActivityHistoryEntry(final int numTransactionsReceived, final int numBlocksDownloaded) { this.numTransactionsReceived = numTransactionsReceived; this.numBlocksDownloaded = numBlocksDownloaded; } @Override public String toString() { return numTransactionsReceived + "/" + numBlocksDownloaded; } } }); impediments = new ImpedimentsLiveData(application); impediments.observe(this, new Observer<Set<Impediment>>() { @Override public void onChanged(final Set<Impediment> impediments) { if (impediments.isEmpty() && peerGroup == null && Constants.ENABLE_BLOCKCHAIN_SYNC) startup(); else if (!impediments.isEmpty() && peerGroup != null) shutdown(); broadcastBlockchainState(); } private void startup() { log.debug("acquiring wakelock"); wakeLock.acquire(); final Wallet wallet = BlockchainService.this.wallet.getValue(); // consistency check final int walletLastBlockSeenHeight = wallet.getLastBlockSeenHeight(); final int bestChainHeight = blockChain.getBestChainHeight(); if (walletLastBlockSeenHeight != -1 && walletLastBlockSeenHeight != bestChainHeight) { final String message = "wallet/blockchain out of sync: " + walletLastBlockSeenHeight + "/" + bestChainHeight; log.error(message); CrashReporter.saveBackgroundTrace(new RuntimeException(message), application.packageInfo()); } peerGroup = new PeerGroup(Constants.NETWORK_PARAMETERS, blockChain); log.info("creating {}", peerGroup); peerGroup.setDownloadTxDependencies(0); // recursive implementation causes StackOverflowError peerGroup.addWallet(wallet); peerGroup.setUserAgent(Constants.USER_AGENT, application.packageInfo().versionName); peerGroup.addConnectedEventListener(peerConnectivityListener); peerGroup.addDisconnectedEventListener(peerConnectivityListener); final int maxConnectedPeers = application.maxConnectedPeers(); final String trustedPeerHost = config.getTrustedPeerHost(); final boolean hasTrustedPeer = trustedPeerHost != null; final boolean connectTrustedPeerOnly = hasTrustedPeer && config.getTrustedPeerOnly(); peerGroup.setMaxConnections(connectTrustedPeerOnly ? 1 : maxConnectedPeers); peerGroup.setConnectTimeoutMillis(Constants.PEER_TIMEOUT_MS); peerGroup.setPeerDiscoveryTimeoutMillis(Constants.PEER_DISCOVERY_TIMEOUT_MS); peerGroup.addPeerDiscovery(new PeerDiscovery() { private final PeerDiscovery normalPeerDiscovery = MultiplexingDiscovery .forServices(Constants.NETWORK_PARAMETERS, 0); @Override public InetSocketAddress[] getPeers(final long services, final long timeoutValue, final TimeUnit timeoutUnit) throws PeerDiscoveryException { final List<InetSocketAddress> peers = new LinkedList<InetSocketAddress>(); boolean needsTrimPeersWorkaround = false; if (hasTrustedPeer) { log.info("trusted peer '" + trustedPeerHost + "'" + (connectTrustedPeerOnly ? " only" : "")); final InetSocketAddress addr = new InetSocketAddress(trustedPeerHost, Constants.NETWORK_PARAMETERS.getPort()); if (addr.getAddress() != null) { peers.add(addr); needsTrimPeersWorkaround = true; } } if (!connectTrustedPeerOnly) peers.addAll(Arrays .asList(normalPeerDiscovery.getPeers(services, timeoutValue, timeoutUnit))); // workaround because PeerGroup will shuffle peers if (needsTrimPeersWorkaround) while (peers.size() >= maxConnectedPeers) peers.remove(peers.size() - 1); return peers.toArray(new InetSocketAddress[0]); } @Override public void shutdown() { normalPeerDiscovery.shutdown(); } }); // start peergroup log.info("starting {} asynchronously", peerGroup); peerGroup.startAsync(); peerGroup.startBlockChainDownload(blockchainDownloadListener); } private void shutdown() { final Wallet wallet = BlockchainService.this.wallet.getValue(); peerGroup.removeDisconnectedEventListener(peerConnectivityListener); peerGroup.removeConnectedEventListener(peerConnectivityListener); peerGroup.removeWallet(wallet); log.info("stopping {} asynchronously", peerGroup); peerGroup.stopAsync(); peerGroup = null; log.debug("releasing wakelock"); wakeLock.release(); } }); } @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { super.onStartCommand(intent, flags, startId); if (intent != null) { log.info("service start command: " + intent + (intent.hasExtra(Intent.EXTRA_ALARM_COUNT) ? " (alarm count: " + intent.getIntExtra(Intent.EXTRA_ALARM_COUNT, 0) + ")" : "")); final String action = intent.getAction(); if (BlockchainService.ACTION_CANCEL_COINS_RECEIVED.equals(action)) { notificationCount = 0; notificationAccumulatedAmount = Coin.ZERO; notificationAddresses.clear(); nm.cancel(Constants.NOTIFICATION_ID_COINS_RECEIVED); } else if (BlockchainService.ACTION_RESET_BLOCKCHAIN.equals(action)) { log.info("will remove blockchain on service shutdown"); resetBlockchainOnShutdown = true; stopSelf(); } else if (BlockchainService.ACTION_BROADCAST_TRANSACTION.equals(action)) { final Sha256Hash hash = Sha256Hash .wrap(intent.getByteArrayExtra(BlockchainService.ACTION_BROADCAST_TRANSACTION_HASH)); final Transaction tx = application.getWallet().getTransaction(hash); if (peerGroup != null) { log.info("broadcasting transaction " + tx.getHashAsString()); peerGroup.broadcastTransaction(tx); } else { log.info("peergroup not available, not broadcasting transaction " + tx.getHashAsString()); } } } else { log.warn("service restart, although it was started as non-sticky"); } return START_NOT_STICKY; } @Override public void onDestroy() { log.debug(".onDestroy()"); if (peerGroup != null) { peerGroup.removeDisconnectedEventListener(peerConnectivityListener); peerGroup.removeConnectedEventListener(peerConnectivityListener); peerGroup.removeWallet(wallet.getValue()); peerGroup.stopAsync(); log.info("stopping {} asynchronously", peerGroup); } peerConnectivityListener.stop(); delayHandler.removeCallbacksAndMessages(null); if (blockStore != null) { try { blockStore.close(); } catch (final BlockStoreException x) { throw new RuntimeException(x); } } application.autosaveWalletNow(); if (wakeLock.isHeld()) { log.debug("wakelock still held, releasing"); wakeLock.release(); } if (resetBlockchainOnShutdown) { log.info("removing blockchain"); blockChainFile.delete(); } scheduleStart(application); stopForeground(true); super.onDestroy(); log.info( "service was up for " + ((System.currentTimeMillis() - serviceCreatedAt) / 1000 / 60) + " minutes"); } @Override public void onTrimMemory(final int level) { log.info("onTrimMemory({}) called", level); if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { log.warn("low memory detected, stopping service"); stopSelf(); } } @Nullable public BlockchainState getBlockchainState() { if (blockChain == null) return null; final StoredBlock chainHead = blockChain.getChainHead(); final Date bestChainDate = chainHead.getHeader().getTime(); final int bestChainHeight = chainHead.getHeight(); final boolean replaying = chainHead.getHeight() < config.getBestChainHeightEver(); return new BlockchainState(bestChainDate, bestChainHeight, replaying, impediments.getValue()); } @Nullable public List<Peer> getConnectedPeers() { if (peerGroup == null) return null; return peerGroup.getConnectedPeers(); } @Nullable public List<StoredBlock> getRecentBlocks(final int maxBlocks) { if (blockChain == null) return null; final List<StoredBlock> blocks = new ArrayList<StoredBlock>(maxBlocks); try { StoredBlock block = blockChain.getChainHead(); while (block != null) { blocks.add(block); if (blocks.size() >= maxBlocks) break; block = block.getPrev(blockStore); } return blocks; } catch (final BlockStoreException x) { throw new RuntimeException(x); } } private void broadcastPeerState(final int numPeers) { final Intent broadcast = new Intent(ACTION_PEER_STATE); broadcast.putExtra(ACTION_PEER_STATE_NUM_PEERS, numPeers); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } private void broadcastBlockchainState() { final Intent broadcast = new Intent(ACTION_BLOCKCHAIN_STATE); final BlockchainState blockchainState = getBlockchainState(); if (blockchainState != null) blockchainState.putExtras(broadcast); LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); } }