crea.wallet.lite.service.CreativeCoinService.java Source code

Java tutorial

Introduction

Here is the source code for crea.wallet.lite.service.CreativeCoinService.java

Source

/*
 * 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 crea.wallet.lite.service;

import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
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.graphics.BitmapFactory;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.os.AsyncTask;
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.util.Log;
import android.widget.TextView;

import com.google.common.util.concurrent.FutureCallback;

import crea.wallet.lite.R;
import crea.wallet.lite.application.Configuration;
import crea.wallet.lite.application.Constants;
import crea.wallet.lite.application.WalletApplication;
import crea.wallet.lite.broadcast.BlockchainBroadcastReceiver;
import crea.wallet.lite.connection.ConnectedPeer;
import crea.wallet.lite.ui.base.TransactionActivity;
import crea.wallet.lite.ui.main.MainActivity;
import crea.wallet.lite.util.wrapper.TimeUtils;
import crea.wallet.lite.util.wrapper.TxInfo;
import crea.wallet.lite.wallet.WalletHelper;

import org.creativecoinj.core.Block;
import org.creativecoinj.core.BlockChain;
import org.creativecoinj.core.CheckpointManager;
import org.creativecoinj.core.Coin;
import org.creativecoinj.core.FilteredBlock;
import org.creativecoinj.core.Message;
import org.creativecoinj.core.Peer;
import org.creativecoinj.core.PeerGroup;
import org.creativecoinj.core.RejectMessage;
import org.creativecoinj.core.RejectedTransactionException;
import org.creativecoinj.core.Sha256Hash;
import org.creativecoinj.core.StoredBlock;
import org.creativecoinj.core.Transaction;
import org.creativecoinj.core.TransactionBroadcast;
import org.creativecoinj.core.TransactionConfidence.ConfidenceType;
import org.creativecoinj.core.Utils;
import org.creativecoinj.core.listeners.DownloadProgressTracker;
import org.creativecoinj.core.listeners.PeerConnectedEventListener;
import org.creativecoinj.core.listeners.PeerDisconnectedEventListener;
import org.creativecoinj.core.listeners.PreMessageReceivedEventListener;
import org.creativecoinj.net.discovery.DnsAndSeedPeerDiscovery;
import org.creativecoinj.store.BlockStore;
import org.creativecoinj.store.BlockStoreException;
import org.creativecoinj.store.SPVBlockStore;
import org.creativecoinj.utils.Threading;
import org.creativecoinj.wallet.Wallet;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import javax.annotation.Nullable;

import static crea.wallet.lite.application.Constants.WALLET.CONTEXT;
import static crea.wallet.lite.application.Constants.WALLET.NETWORK_PARAMETERS;
import static crea.wallet.lite.broadcast.BlockchainBroadcastReceiver.ACTION_SYNC_STARTED;
import static crea.wallet.lite.broadcast.BlockchainBroadcastReceiver.TRANSACTION_REJECTED;

/**
 * @author Andreas Schildbach
 */
public class CreativeCoinService extends Service implements BlockchainService {

    private static final String TAG = "CreativeCoinService";
    private static final int NOTIFICATION_ID_CONNECTED = 0;
    private static final int NOTIFICATION_ID_COINS_RECEIVED = 1;
    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 = 5;
    private static final int MAX_HISTORY_SIZE = Math.max(IDLE_TRANSACTION_TIMEOUT_MIN, IDLE_BLOCK_TIMEOUT_MIN);

    public static TextView progressBar;

    private final AbstractWalletCoinListener WALLET_COIN_LISTENER = new AbstractWalletCoinListener() {
        @Override
        public void onCoinsReceived(Wallet wallet, final Transaction tx, Coin coin, Coin coin1) {
            org.creativecoinj.core.Context.propagate(CONTEXT);

            Log.e(TAG, "Coins received! " + tx.toString());

            TxInfo txInfo = new TxInfo(tx);

            if (!txInfo.isSentFromUser()) {
                final int bestChainHeight = blockChain.getBestChainHeight();

                final Coin amount = txInfo.getAmountReceived();
                final ConfidenceType confidenceType = tx.getConfidence().getConfidenceType();

                WalletHelper.INSTANCE.save();
                //TO VERIFY ONLY NEW INPUT TRANSACTIONS
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        final boolean isReceived = amount.signum() > 0;
                        final boolean replaying = bestChainHeight < Configuration.getInstance()
                                .getBestChainHeightEver();
                        final boolean isReplayedTx = confidenceType == ConfidenceType.BUILDING && replaying;

                        if (isReceived && !isReplayedTx) {
                            transactionsReceived.put(tx.getHash(), Coin.valueOf(amount.longValue()));
                            //NOTIFY INPUT TRANSACTION
                            String id = tx.getHashAsString();
                            notifyCoinsReceived(tx.getHash());
                            broadcastTransactionReceived(id);
                        }
                    }
                });
            }
        }

        @Override
        public void onCoinsSent(Wallet wallet, Transaction tx, Coin coin, Coin coin1) {
            String id = tx.getHashAsString();
            broadcastTransactionSend(id);
        }
    };

    private final Set<BlockchainState.Impediment> impediments = EnumSet.noneOf(BlockchainState.Impediment.class);
    private final Handler handler = new Handler();
    private final Handler delayHandler = new Handler();
    private final Handler rateHandler = new Handler();

    private WalletApplication application;

    private BlockStore blockStore;
    private File blockChainFile;
    private BlockChain blockChain;

    private PeerGroup peerGroup;
    private WakeLock wakeLock;
    private PeerConnectivityListener peerConnectivityListener;
    private NotificationManager nm;
    private Coin notificationAccumulatedAmount = Coin.ZERO;
    private HashMap<Sha256Hash, Coin> transactionsReceived;
    private long serviceCreatedAt;
    private boolean resetBlockchainOnShutdown = false;

    private void broadcastTransactionSend(String id) {
        notifyTransaction(id, BlockchainBroadcastReceiver.TRANSACTION_SENT);
    }

    private void broadcastTransactionReceived(String id) {
        notifyTransaction(id, BlockchainBroadcastReceiver.TRANSACTION_RECEIVED);
    }

    private void notifyTransaction(String id, String action) {
        Intent sendIntent = new Intent(action);
        sendIntent.putExtra("txId", id);
        sendBroadcast(sendIntent);
    }

    private void notifyCoinsReceived(Sha256Hash hash) {
        if (!Configuration.getInstance().isNotificationsEnabled()) {
            return;
        }

        int notificationCount = transactionsReceived.size();
        if (notificationCount == 1) {
            nm.cancel(NOTIFICATION_ID_COINS_RECEIVED);
        }

        notificationAccumulatedAmount = Coin.ZERO;
        for (Coin c : transactionsReceived.values()) {
            notificationAccumulatedAmount = notificationAccumulatedAmount.add(c);
        }

        Coin coin = Coin.valueOf(notificationAccumulatedAmount.getValue());
        String msg;
        Intent intent;

        msg = getString(R.string.notif_accumulated_amount, coin.toFriendlyString(), notificationCount);
        intent = new Intent(this, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

        intent.putExtra(TransactionActivity.TRANSACTION_ID, hash.toString());

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

        String title = getString(R.string.notif_cash_in_title, coin.toFriendlyString());
        final NotificationCompat.Builder notification = new NotificationCompat.Builder(this);
        notification.setSmallIcon(R.mipmap.ic_notification);
        notification.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        notification.setColor(getResources().getColor(R.color.colorPrimary));
        notification.setContentText(msg);
        notification.setContentTitle(title);
        notification.setWhen(System.currentTimeMillis());
        notification.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION));
        notification.setContentIntent(pendingIntent);
        Notification notif = notification.build();
        notif.flags |= Notification.FLAG_AUTO_CANCEL;
        nm.notify(NOTIFICATION_ID_COINS_RECEIVED, notif);
    }

    private final class PeerConnectivityListener
            implements PeerConnectedEventListener, PeerDisconnectedEventListener, OnSharedPreferenceChangeListener {

        private AtomicBoolean stopped = new AtomicBoolean(false);

        public PeerConnectivityListener() {
            Configuration.getInstance().registerOnSharedPreferenceChangeListener(this);
        }

        public void stop() {
            stopped.set(true);
            Configuration.getInstance().unregisterOnSharedPreferenceChangeListener(this);

            //REMOVE NOTIFICATIONS?
            nm.cancel(NOTIFICATION_ID_CONNECTED);
        }

        @Override
        public void onPeerConnected(final Peer peer, final int peerCount) {
            Log.i(TAG, "Connected " + peer.getAddress().toString());
            broadcastPeers();
        }

        @Override
        public void onPeerDisconnected(final Peer peer, final int peerCount) {
            Log.i(TAG, "Disconnected " + peer.getAddress().toString());
            broadcastPeers();
        }

        @Override
        public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
        }

    }

    private final DownloadProgressTracker blockchainDownloadListener = new DownloadProgressTracker() {
        private final long CALLBACK_TIME = 1000L;
        private final AtomicLong lastMessageTime = new AtomicLong(0);

        private int remain;
        private String remainingTime = "";

        private int downloaded = 0;
        private int time = 0;
        private int blockSpeed = 0;
        private boolean rateStarted = false;

        @Override
        public void onChainDownloadStarted(Peer peer, int blocksLeft) {
            broadcastPendingTx();
            if (!rateStarted) {
                rateStarted = true;
                rateHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        time++;
                        downloaded = downloaded + blockSpeed;
                        double average = downloaded / time;
                        if (average > 0) {
                            long remainTime = Math.round(remain / average * 1000L);
                            remainingTime = TimeUtils.toHumanReadable(remainTime);
                        }

                        Log.d(TAG, remainingTime + ", REMAINS=" + remain + ", " + blockSpeed + " blocks/s");
                        blockSpeed = 0;
                        if (progressBar != null) {
                            progressBar.post(new Runnable() {
                                @Override
                                public void run() {
                                    try {
                                        progressBar.setText(getString(R.string.sync_time, remainingTime));
                                    } catch (Exception e) {
                                        Log.e(TAG, "Unable to show progress");
                                    }
                                }
                            });
                        }

                        rateHandler.postDelayed(this, 1000);
                    }
                });
            }
            super.onChainDownloadStarted(peer, blocksLeft);
        }

        @Override
        protected void doneDownload() {
            Log.d(TAG, "doneDownload");
            Coin estimated = WalletHelper.INSTANCE.getBalance(Wallet.BalanceType.ESTIMATED);
            Coin available = WalletHelper.INSTANCE.getBalance(Wallet.BalanceType.AVAILABLE);
            Coin pending = estimated.minus(available);

            Log.d(TAG, "ESTIMATED = " + estimated.toFriendlyString());
            Log.d(TAG, "AVAILABLE = " + available.toFriendlyString());
            Log.d(TAG, "PENDING = " + pending.toFriendlyString());
            //WalletHelper.INSTANCE.cleanup();
            sendBroadcast(new Intent(BlockchainBroadcastReceiver.LAST_BLOCK_RECEIVED));
            rateHandler.removeCallbacksAndMessages(null);
            delayHandler.removeCallbacksAndMessages(null);
            Configuration.getInstance().maybeIncrementBestChainHeightEver(blockChain.getChainHead().getHeight());

        }

        @Override
        public void onBlocksDownloaded(final Peer peer, final Block block, final FilteredBlock filteredBlock,
                final int blocksLeft) {
            super.onBlocksDownloaded(peer, block, filteredBlock, blocksLeft);
            this.remain = blocksLeft;
            blockSpeed++;

            delayHandler.removeCallbacksAndMessages(null);
            //         refreshConfirmations(height, block);

            final long now = System.currentTimeMillis();

            if (now - lastMessageTime.get() > CALLBACK_TIME) {
                delayHandler.post(RUNNER);
            } else {
                delayHandler.postDelayed(RUNNER, CALLBACK_TIME);
            }
        }

        private final Runnable RUNNER = new Runnable() {
            @Override
            public void run() {
                lastMessageTime.set(System.currentTimeMillis());
                Configuration.getInstance()
                        .maybeIncrementBestChainHeightEver(blockChain.getChainHead().getHeight());
            }
        };
    };

    private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(final Context context, final Intent intent) {
            final String action = intent.getAction();

            Log.e(TAG, "Context: " + context.getClass().getName() + ", action: " + action);
            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
                final boolean hasConnectivity = !intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY,
                        false);
                Log.i(TAG, "network is " + (hasConnectivity ? "up" : "down"));

                if (hasConnectivity) {
                    impediments.remove(BlockchainState.Impediment.NETWORK);
                } else {
                    impediments.add(BlockchainState.Impediment.NETWORK);
                }

                check();
            } else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
                Log.i(TAG, "device storage low");
                impediments.add(BlockchainState.Impediment.STORAGE);
                check();
            } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
                Log.i(TAG, "device storage ok");
                impediments.remove(BlockchainState.Impediment.STORAGE);
                check();
            }
        }

        @SuppressLint("Wakelock")
        private void check() {

            if (impediments.isEmpty() && peerGroup == null) {
                Log.d(TAG, "acquiring wakelock");
                wakeLock.acquire();

                // consistency check
                final int walletLastBlockSeenHeight = WalletHelper.INSTANCE.getLastBlockHeight();
                final int bestChainHeight = blockChain.getBestChainHeight();
                if (walletLastBlockSeenHeight != -1 && walletLastBlockSeenHeight != bestChainHeight) {
                    final String message = "wallet/blockchain out of sync: " + walletLastBlockSeenHeight + "/"
                            + bestChainHeight;
                    Log.e(TAG, message);
                }

                org.creativecoinj.core.Context.propagate(CONTEXT);

                Log.i(TAG, "starting peergroup");
                peerGroup = new PeerGroup(NETWORK_PARAMETERS, blockChain);
                peerGroup.setDownloadTxDependencies(0); // recursive implementation causes StackOverflowError
                peerGroup.setBloomFilteringEnabled(true);
                peerGroup.setMinBroadcastConnections(2);
                addWallets();

                peerGroup.setUserAgent(Constants.APP.CLIENT_NAME, Constants.APP.VERSION);
                peerGroup.addConnectedEventListener(peerConnectivityListener);
                peerGroup.addDisconnectedEventListener(peerConnectivityListener);

                final int maxConnectedPeers = application.maxConnectedPeers();

                peerGroup.setMaxConnections(maxConnectedPeers);
                peerGroup.setConnectTimeoutMillis(Constants.PEER_TIMEOUT_MS);
                peerGroup.setPeerDiscoveryTimeoutMillis(Constants.PEER_DISCOVERY_TIMEOUT_MS);

                peerGroup.addPeerDiscovery(new DnsAndSeedPeerDiscovery(NETWORK_PARAMETERS));

                new AsyncTask<Void, Void, Void>() {

                    @Override
                    protected Void doInBackground(Void... params) {
                        // start peergroup
                        Log.e(TAG, "Starting blockchain download...");
                        peerGroup.startAsync();
                        peerGroup.startBlockChainDownload(blockchainDownloadListener);
                        return null;
                    }

                    @Override
                    protected void onPostExecute(Void aVoid) {
                        super.onPostExecute(aVoid);
                        broadcastSyncStarted();
                    }
                }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

            } else if (!impediments.isEmpty() && peerGroup != null) {
                Log.i(TAG, "stopping peergroup");
                peerGroup.removeDisconnectedEventListener(peerConnectivityListener);
                peerGroup.removeConnectedEventListener(peerConnectivityListener);
                removeWallets();
                peerGroup.stopAsync();
                peerGroup = null;

                Log.d(TAG, "releasing wakelock");
                wakeLock.release();
            }
        }
    };

    private final static 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;
        }
    }

    private final BroadcastReceiver tickReceiver = new BroadcastReceiver() {
        private int lastChainHeight = 0;
        private final List<ActivityHistoryEntry> activityHistory = new LinkedList<ActivityHistoryEntry>();

        @Override
        public void onReceive(final Context context, final Intent intent) {
            final int chainHeight = blockChain.getBestChainHeight();

            if (lastChainHeight > 0) {
                final int numBlocksDownloaded = chainHeight - lastChainHeight;
                final int numTransactionsReceived = transactionsReceived.size();
                transactionsReceived.clear();

                // 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.i(TAG, "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 (Configuration.getInstance().isIdleDetectionEnabled() && isIdle) {
                    Log.i(TAG, "idling detected, stopping service");
                    stopSelf();
                }
            }

            lastChainHeight = chainHeight;
        }
    };

    private void addWallets() {
        if (peerGroup != null) {
            WalletHelper.INSTANCE.addIssuedAddressesToWatch();
            WalletHelper.INSTANCE.addEventListener(WALLET_COIN_LISTENER, Threading.THREAD_POOL);
            peerGroup.addWallet(WalletHelper.INSTANCE.getWallet());

            Log.e(TAG, "Watching wallets");
        }
    }

    private void removeWallets() {
        if (peerGroup != null) {
            WalletHelper.INSTANCE.removeEventListener(WALLET_COIN_LISTENER);

            peerGroup.removeWallet(WalletHelper.INSTANCE.getWallet());

            Log.e(TAG, "Removed wallet");
        }
    }

    public class LocalBinder extends Binder {
        public BlockchainService getService() {
            return CreativeCoinService.this;
        }
    }

    private final IBinder mBinder = new LocalBinder();

    @Override
    public IBinder onBind(final Intent intent) {
        Log.e(TAG, ".onBind()");
        return mBinder;
    }

    @Override
    public void onCreate() {
        serviceCreatedAt = System.currentTimeMillis();
        super.onCreate();

        boolean startService = WalletHelper.INSTANCE != null;

        transactionsReceived = new HashMap<>();
        Log.e(TAG, "START_SERVICE=" + startService);
        if (startService) {
            nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

            WalletHelper.INSTANCE.autoSave(10);
            final String lockName = getPackageName() + " blockchain sync";

            final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName);
            application = WalletApplication.INSTANCE;

            setWalletListener();

            peerConnectivityListener = new PeerConnectivityListener();

            blockChainFile = Constants.WALLET.BLOCKCHAIN_FILE;
            final boolean blockChainFileExists = blockChainFile.exists();

            if (!blockChainFileExists) {
                new File(Constants.WALLET.WALLET_PATH).mkdirs();
            }
            try {
                blockStore = new SPVBlockStore(NETWORK_PARAMETERS, blockChainFile);
                blockStore.getChainHead(); // detect corruptions as early as possible

                final long earliestKeyCreationTime = WalletHelper.INSTANCE.getKeyCreationTime();

                Log.e(TAG, "CREATION_TIME=" + earliestKeyCreationTime + " BLOCKCHAIN_FILE=" + blockChainFileExists);
                if (!blockChainFileExists && earliestKeyCreationTime > 0) {
                    try {
                        Configuration.getInstance().setDeletingBlockchain(true);
                        final long start = System.currentTimeMillis();
                        final InputStream checkpointsInputStream = getAssets()
                                .open("bitcoin/" + Constants.WALLET.CHECKPOINTS_FILENAME);
                        CheckpointManager.checkpoint(NETWORK_PARAMETERS, checkpointsInputStream, blockStore,
                                earliestKeyCreationTime);
                        Log.e(TAG, String.format("checkpoints loaded from '%1$s', took %2$dms",
                                Constants.WALLET.CHECKPOINTS_FILENAME, System.currentTimeMillis() - start));
                    } catch (final IOException x) {
                        Log.e(TAG, "problem reading checkpoints, continuing without", x);
                    }
                }
            } catch (final BlockStoreException x) {
                blockChainFile.delete();

                final String msg = "blockstore cannot be created";
                Log.e(TAG, msg, x);
            }

            try {
                blockChain = new BlockChain(NETWORK_PARAMETERS, WalletHelper.INSTANCE.getWallet(), blockStore);
            } catch (final BlockStoreException x) {
                throw new Error("blockchain cannot be created", x);
            }

            final IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
            intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
            intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
            registerReceiver(connectivityReceiver, intentFilter); // implicitly start PeerGroup

            registerReceiver(tickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
        } else {
            Log.e(TAG, "Incomplete conditions for start service");
            stopSelf();
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand");

        org.creativecoinj.core.Context.propagate(CONTEXT);
        if (intent != null) {
            Log.i(TAG,
                    "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 (ACTION_CANCEL_COINS_RECEIVED.equals(action)) {
                notificationAccumulatedAmount = Coin.ZERO;
                transactionsReceived.clear();

                if (nm != null) {
                    nm.cancel(NOTIFICATION_ID_COINS_RECEIVED);
                }

            } else if (ACTION_BROADCAST_TRANSACTION.equals(action)) {
                broadcastTransactionPendingTx(intent);
            } else if (ACTION_RESET_BLOCKCHAIN.equals(action)) {
                Log.i(TAG, "will remove blockchain on service shutdown");

                resetBlockchainOnShutdown = true;
                stopSelf();
            } else if (ACTION_SEND_PEERS.equals(action)) {
                broadcastPeers();
            } else if (peerGroup != null && peerGroup.isRunning()) {
                broadcastSyncStarted();
            }
        } else {
            Log.e(TAG, "service restart, although it was started as non-sticky");
        }
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, ".onDestroy()");

        WalletApplication.scheduleStartBlockchainService(this);
        unregisterReceiver(tickReceiver);

        WalletHelper.INSTANCE.removeEventListener(WALLET_COIN_LISTENER);
        unregisterReceiver(connectivityReceiver);

        if (peerGroup != null) {
            peerGroup.removeDisconnectedEventListener(peerConnectivityListener);
            peerGroup.removeConnectedEventListener(peerConnectivityListener);
            peerGroup.removeWallet(WalletHelper.INSTANCE.getWallet());
            try {
                peerGroup.stop();
            } catch (IllegalStateException e) {
                Log.e(TAG, "PeerGroup started? " + peerGroup.isRunning(), e);
            }

            Log.i(TAG, "peergroup stopped");
        }

        peerConnectivityListener.stop();
        delayHandler.removeCallbacksAndMessages(null);
        handler.removeCallbacksAndMessages(null);
        rateHandler.removeCallbacksAndMessages(null);

        try {
            blockStore.close();
        } catch (final BlockStoreException x) {
            throw new RuntimeException(x);
        }

        removeWalletListener();
        WalletHelper.INSTANCE.save();

        if (wakeLock.isHeld()) {
            Log.d(TAG, "wakelock still held, releasing");
            wakeLock.release();
        }

        if (resetBlockchainOnShutdown) {
            Log.i(TAG, "removing blockchain");
            blockChainFile.delete();
            WalletHelper.INSTANCE.reset();
            broadCastBlockChainReset();
        }

        super.onDestroy();

        Log.i(TAG,
                "service was up for " + ((System.currentTimeMillis() - serviceCreatedAt) / 1000 / 60) + " minutes");
    }

    @Override
    public void onTrimMemory(final int level) {
        Log.i(TAG, String.format("onTrimMemory(%1$d) called", level));

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            Log.d(TAG, "low memory detected, stopping service");
            stopSelf();
        }
    }

    @Override
    public BlockchainState getBlockchainState() {
        final StoredBlock chainHead = blockChain.getChainHead();
        final Date bestChainDate = chainHead.getHeader().getTime();
        final int bestChainHeight = chainHead.getHeight();
        final boolean replaying = chainHead.getHeight() < Configuration.getInstance().getBestChainHeightEver();

        return new BlockchainState(bestChainDate, bestChainHeight, replaying, impediments);
    }

    @Override
    public List<Peer> getConnectedPeers() {
        if (peerGroup != null) {
            return peerGroup.getConnectedPeers();
        } else {
            return null;
        }
    }

    @Override
    public List<StoredBlock> getRecentBlocks(final int maxBlocks) {
        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);
            }
        } catch (final BlockStoreException x) {
            // swallow
        }

        return blocks;
    }

    public void broadcastTransactionPendingTx(Intent data) {
        if (peerGroup != null) {
            byte[] rawTx = data.getByteArrayExtra(ACTION_BROADCAST_RAW_TRANSACTION);

            Transaction tx = new Transaction(NETWORK_PARAMETERS, rawTx);
            final Sha256Hash hash = tx.getHash();

            Log.i(TAG, "broadcasting transaction " + hash.toString());
            TransactionBroadcast tb = peerGroup.broadcastTransaction(tx);
            tb.setTransactionCallback(new FutureCallback<Transaction>() {
                @Override
                public void onSuccess(@Nullable Transaction transaction) {
                    WalletHelper.INSTANCE.commitTx(transaction);
                }

                @Override
                public void onFailure(Throwable throwable) {
                    if (throwable instanceof RejectedTransactionException) {
                        RejectedTransactionException rte = (RejectedTransactionException) throwable;
                        Log.e(TAG, "Transaction rejected! " + rte.getTransaction().getHashAsString());

                        Intent rejectIntent = new Intent(TRANSACTION_REJECTED);
                        rejectIntent.putExtra("rawTx", rte.getTransaction().bitcoinSerialize());

                        sendBroadcast(rejectIntent);
                    }
                }
            });

        } else {
            Log.i(TAG, "Tx not available");
        }
    }

    public void broadCastBlockChainReset() {
        Intent i = new Intent(BlockchainBroadcastReceiver.BLOCKCHAIN_RESET);
        sendBroadcast(i);
    }

    private void setWalletListener() {
        WalletHelper.INSTANCE.addEventListener(WALLET_COIN_LISTENER, Threading.SAME_THREAD);
    }

    private void removeWalletListener() {
        WalletHelper.INSTANCE.removeEventListener(WALLET_COIN_LISTENER);
    }

    private void broadcastPendingTx() {
        List<Transaction> pendings = WalletHelper.INSTANCE.getPendingTransactions();
        Log.d(TAG, "broadcasting " + pendings.size() + " transactions");
        for (Transaction tx : pendings) {
            peerGroup.broadcastTransaction(tx);
        }
    }

    private void broadcastSyncStarted() {
        sendBroadcast(new Intent(ACTION_SYNC_STARTED));
    }

    private void broadcastPeers() {
        if (peerGroup != null && peerGroup.isRunning()) {
            new AsyncTask<Void, Void, ArrayList<ConnectedPeer>>() {

                @Override
                protected ArrayList<ConnectedPeer> doInBackground(Void... params) {
                    return ConnectedPeer.wrapAsList(peerGroup.getConnectedPeers());
                }

                @Override
                protected void onPostExecute(ArrayList<ConnectedPeer> peers) {
                    Intent peerIntent = new Intent(BlockchainBroadcastReceiver.ACTION_PEERS_CHANGED);
                    peerIntent.putExtra("peers", peers);
                    Log.e(TAG, "Broadcasting " + peers.size() + " peers");
                    sendBroadcast(peerIntent);
                }
            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

        }
    }
}