/* * Copyright 2011-2013 the original author or authors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.schildbach.wallet.digitalcoin.service; import java.io.*; import java.math.BigInteger; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import android.annotation.SuppressLint; import android.app.NotificationManager; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; 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.Uri; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.BatteryManager; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.text.format.DateUtils; import android.util.Log; import com.google.digitalcoin.core.*; import com.google.digitalcoin.core.TransactionConfidence.ConfidenceType; import com.google.digitalcoin.core.Wallet.BalanceType; import com.google.digitalcoin.discovery.DnsDiscovery; import com.google.digitalcoin.discovery.IrcDiscovery; import com.google.digitalcoin.discovery.PeerDiscovery; import com.google.digitalcoin.discovery.PeerDiscoveryException; import com.google.digitalcoin.store.BlockStore; import com.google.digitalcoin.store.BlockStoreException; import com.google.digitalcoin.store.SPVBlockStore; import de.schildbach.wallet.digitalcoin.Constants; import de.schildbach.wallet.digitalcoin.WalletApplication; import de.schildbach.wallet.digitalcoin.WalletBalanceWidgetProvider; import de.schildbach.wallet.digitalcoin.ui.WalletActivity; import de.schildbach.wallet.digitalcoin.util.ThrottelingWalletChangeListener; import de.schildbach.wallet.digitalcoin.util.WalletUtils; import de.schildbach.wallet.digitalcoin.R; /** * @author Andreas Schildbach */ public class BlockchainServiceImpl extends android.app.Service implements BlockchainService { private WalletApplication application; private SharedPreferences prefs; private BlockStore blockStore; private File blockChainFile; private BlockChain blockChain; private PeerGroup peerGroup; private final Handler handler = new Handler(); private final Handler delayHandler = new Handler(); private WakeLock wakeLock; private WifiLock wifiLock; private PeerConnectivityListener peerConnectivityListener; private NotificationManager nm; private static final int NOTIFICATION_ID_CONNECTED = 0; private static final int NOTIFICATION_ID_COINS_RECEIVED = 1; private int notificationCount = 0; private BigInteger notificationAccumulatedAmount = BigInteger.ZERO; private final List<Address> notificationAddresses = new LinkedList<Address>(); private int bestChainHeightEver; private boolean resetBlockchainOnShutdown = false; private static final int MAX_LAST_CHAIN_HEIGHTS = 10; private static final int IDLE_TIMEOUT_MIN = 2; private static final long APPWIDGET_THROTTLE_MS = DateUtils.SECOND_IN_MILLIS; private static final String TAG = "digitalcoin"+BlockchainServiceImpl.class.getSimpleName(); private final WalletEventListener walletEventListener = new ThrottelingWalletChangeListener(APPWIDGET_THROTTLE_MS) { @Override public void onThrotteledWalletChanged() { notifyWidgets(); } @Override public void onCoinsReceived(final Wallet wallet, final Transaction tx, final BigInteger prevBalance, final BigInteger newBalance) { try { final Address from; if (!tx.isCoinBase()) { final TransactionInput input = tx.getInputs().get(0); from = input.getFromAddress(); } else { from = null; } final BigInteger amount = tx.getValue(wallet); final ConfidenceType confidenceType = tx.getConfidence().getConfidenceType(); handler.post(new Runnable() { public void run() { final boolean isReceived = amount.signum() > 0; final int bestChainHeight = blockChain.getBestChainHeight(); final boolean replaying = bestChainHeight < bestChainHeightEver; final boolean isReplayedTx = confidenceType == ConfidenceType.BUILDING && replaying; if (isReceived && !isReplayedTx) notifyCoinsReceived(from, amount); } }); } catch (final ScriptException x) { throw new RuntimeException(x); } } }; private void notifyCoinsReceived(final Address from, final BigInteger amount) { if (notificationCount == 1) nm.cancel(NOTIFICATION_ID_COINS_RECEIVED); notificationCount++; notificationAccumulatedAmount = notificationAccumulatedAmount.add(amount); if (from != null && !notificationAddresses.contains(from)) notificationAddresses.add(from); final int precision = Integer.parseInt(prefs.getString(Constants.PREFS_KEY_WDC_PRECISION, Integer.toString(Constants.WDC_PRECISION))); final String tickerMsg = getString(R.string.notification_coins_received_msg, WalletUtils.formatValue(amount, precision)) + Constants.NETWORK_SUFFIX; final String msg = getString(R.string.notification_coins_received_msg, WalletUtils.formatValue(notificationAccumulatedAmount, precision)) + Constants.NETWORK_SUFFIX; final StringBuilder text = new StringBuilder(); for (final Address address : notificationAddresses) { if (text.length() > 0) text.append(", "); text.append(address.toString()); } if (text.length() == 0) text.append("unknown"); text.insert(0, "From "); final NotificationCompat.Builder notification = new NotificationCompat.Builder(this); notification.setSmallIcon(R.drawable.stat_notify_received); notification.setTicker(tickerMsg); notification.setContentTitle(msg); notification.setContentText(text); notification.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, WalletActivity.class), 0)); notification.setNumber(notificationCount == 1 ? 0 : notificationCount); notification.setWhen(System.currentTimeMillis()); notification.setSound(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.coins_received)); nm.notify(NOTIFICATION_ID_COINS_RECEIVED, notification.getNotification()); } private class PeerConnectivityListener extends AbstractPeerEventListener implements OnSharedPreferenceChangeListener { private int peerCount; private AtomicBoolean stopped = new AtomicBoolean(false); public PeerConnectivityListener() { prefs.registerOnSharedPreferenceChangeListener(this); } public void stop() { stopped.set(true); prefs.unregisterOnSharedPreferenceChangeListener(this); nm.cancel(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); } public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { if (Constants.PREFS_KEY_CONNECTIVITY_NOTIFICATION.equals(key)) changed(peerCount); } private void changed(final int numPeers) { if (stopped.get()) return; handler.post(new Runnable() { public void run() { final boolean connectivityNotification = prefs.getBoolean(Constants.PREFS_KEY_CONNECTIVITY_NOTIFICATION, true); if (!connectivityNotification || numPeers == 0) { nm.cancel(NOTIFICATION_ID_CONNECTED); } else { final NotificationCompat.Builder notification = new NotificationCompat.Builder(BlockchainServiceImpl.this); notification.setSmallIcon(R.drawable.stat_sys_peers, numPeers > 4 ? 4 : numPeers); notification.setContentTitle(getString(R.string.app_name)); notification.setContentText(getString(R.string.notification_peers_connected_msg, numPeers)); notification.setContentIntent(PendingIntent.getActivity(BlockchainServiceImpl.this, 0, new Intent(BlockchainServiceImpl.this, WalletActivity.class), 0)); notification.setWhen(System.currentTimeMillis()); notification.setOngoing(true); nm.notify(NOTIFICATION_ID_CONNECTED, notification.getNotification()); } // send broadcast sendBroadcastPeerState(numPeers); } }); } } private final PeerEventListener blockchainDownloadListener = new AbstractPeerEventListener() { private final AtomicLong lastMessageTime = new AtomicLong(0); @Override public void onBlocksDownloaded(final Peer peer, final Block block, final int blocksLeft) { delayHandler.removeCallbacksAndMessages(null); final long now = System.currentTimeMillis(); if (now - lastMessageTime.get() > Constants.BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS) delayHandler.post(runnable); else delayHandler.postDelayed(runnable, Constants.BLOCKCHAIN_STATE_BROADCAST_THROTTLE_MS); } private final Runnable runnable = new Runnable() { public void run() { lastMessageTime.set(System.currentTimeMillis()); final Date bestChainDate = new Date(blockChain.getChainHead().getHeader().getTimeSeconds() * DateUtils.SECOND_IN_MILLIS); final int bestChainHeight = blockChain.getBestChainHeight(); if (bestChainHeight > bestChainHeightEver) bestChainHeightEver = bestChainHeight; final boolean replaying = bestChainHeight < bestChainHeightEver; sendBroadcastBlockchainState(bestChainDate, bestChainHeight, replaying, ACTION_BLOCKCHAIN_STATE_DOWNLOAD_OK); } }; }; private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() { private boolean hasConnectivity; private boolean hasPower; private boolean hasStorage = true; @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { hasConnectivity = !intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); final String reason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON); // final boolean isFailover = intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false); Log.i(TAG, "network is " + (hasConnectivity ? "up" : "down") + (reason != null ? ": " + reason : "")); check(); } else if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { final int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); final int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); final int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); hasPower = plugged != 0 || level > scale / 10; Log.i(TAG, "battery changed: level=" + level + "/" + scale + " plugged=" + plugged); check(); } else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { hasStorage = false; Log.i(TAG, "device storage low"); check(); } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { hasStorage = true; Log.i(TAG, "device storage ok"); check(); } } @SuppressLint("Wakelock") private void check() { final Wallet wallet = application.getWallet(); final boolean hasEverything = hasConnectivity && hasPower && hasStorage; if (hasEverything && peerGroup == null) { Log.d(TAG, "acquiring wakelock"); wakeLock.acquire(); Log.i(TAG, "starting peergroup"); peerGroup = new PeerGroup(Constants.NETWORK_PARAMETERS, blockChain); try { peerGroup.addWallet(wallet); } catch(NoSuchMethodError e) { Log.e("digitalcoin", "There's no method: " + e.getLocalizedMessage()); Log.e("digitalcoin", "digitalcoinj issue. We're going to ignore this for now and just try and return nicely."); return; } peerGroup.setUserAgent(Constants.USER_AGENT, application.applicationVersionName()); peerGroup.addEventListener(peerConnectivityListener); final int maxConnectedPeers = application.maxConnectedPeers(); final String trustedPeerHost = prefs.getString(Constants.PREFS_KEY_TRUSTED_PEER, "").trim(); final boolean hasTrustedPeer = trustedPeerHost.length() > 0; final boolean connectTrustedPeerOnly = hasTrustedPeer && prefs.getBoolean(Constants.PREFS_KEY_TRUSTED_PEER_ONLY, false); peerGroup.setMaxConnections(connectTrustedPeerOnly ? 1 : maxConnectedPeers); peerGroup.addPeerDiscovery(new PeerDiscovery() { private final PeerDiscovery normalPeerDiscovery = Constants.TEST ? new IrcDiscovery(Constants.PEER_DISCOVERY_IRC_CHANNEL_TEST) : new DnsDiscovery(Constants.NETWORK_PARAMETERS); public InetSocketAddress[] getPeers(final long timeoutValue, final TimeUnit timeoutUnit) throws PeerDiscoveryException { final List<InetSocketAddress> peers = new LinkedList<InetSocketAddress>(); boolean needsTrimPeersWorkaround = false; if (hasTrustedPeer) { final InetSocketAddress addr = new InetSocketAddress(trustedPeerHost, Constants.NETWORK_PARAMETERS.port); if (addr.getAddress() != null) { peers.add(addr); needsTrimPeersWorkaround = true; } } if (!connectTrustedPeerOnly) peers.addAll(Arrays.asList(normalPeerDiscovery.getPeers(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]); } public void shutdown() { normalPeerDiscovery.shutdown(); } }); // start peergroup peerGroup.start(); peerGroup.startBlockChainDownload(blockchainDownloadListener); } else if (!hasEverything && peerGroup != null) { Log.i(TAG, "stopping peergroup"); peerGroup.removeEventListener(peerConnectivityListener); peerGroup.removeWallet(wallet); peerGroup.stop(); peerGroup = null; Log.d(TAG, "releasing wakelock"); wakeLock.release(); } final Date bestChainDate = new Date(blockChain.getChainHead().getHeader().getTimeSeconds() * DateUtils.SECOND_IN_MILLIS); final int bestChainHeight = blockChain.getBestChainHeight(); final int download = (hasConnectivity ? 0 : ACTION_BLOCKCHAIN_STATE_DOWNLOAD_NETWORK_PROBLEM) | (hasPower ? 0 : ACTION_BLOCKCHAIN_STATE_DOWNLOAD_POWER_PROBLEM) | (hasStorage ? 0 : ACTION_BLOCKCHAIN_STATE_DOWNLOAD_STORAGE_PROBLEM); final boolean replaying = bestChainHeight < bestChainHeightEver; sendBroadcastBlockchainState(bestChainDate, bestChainHeight, replaying, download); } }; private final BroadcastReceiver tickReceiver = new BroadcastReceiver() { private int lastChainHeight = 0; private final List<Integer> lastDownloadedHistory = new LinkedList<Integer>(); @Override public void onReceive(final Context context, final Intent intent) { final int chainHeight = blockChain.getBestChainHeight(); if (lastChainHeight > 0) { final int downloaded = chainHeight - lastChainHeight; // push number of downloaded blocks lastDownloadedHistory.add(0, downloaded); // trim while (lastDownloadedHistory.size() > MAX_LAST_CHAIN_HEIGHTS) lastDownloadedHistory.remove(lastDownloadedHistory.size() - 1); // print final StringBuilder builder = new StringBuilder(); for (final int lastDownloaded : lastDownloadedHistory) { if (builder.length() > 0) builder.append(','); builder.append(lastDownloaded); } Log.i(TAG, "Number of blocks downloaded: " + builder); // determine if download is idling boolean isIdle = false; if (lastDownloadedHistory.size() >= IDLE_TIMEOUT_MIN) { isIdle = true; for (int i = 0; i < IDLE_TIMEOUT_MIN; i++) { if (lastDownloadedHistory.get(i) > 0) { isIdle = false; break; } } } // if idling, shutdown service if (isIdle) { Log.i(TAG, "end of block download detected, stopping service"); stopSelf(); } } lastChainHeight = chainHeight; } }; public class LocalBinder extends Binder { public BlockchainService getService() { return BlockchainServiceImpl.this; } } private final IBinder mBinder = new LocalBinder(); @Override public IBinder onBind(final Intent intent) { Log.d(TAG, ".onBind()"); return mBinder; } @Override public boolean onUnbind(final Intent intent) { Log.d(TAG, ".onUnbind()"); return super.onUnbind(intent); } @Override public void onCreate() { Log.d(TAG, ".onCreate()"); super.onCreate(); nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); final String lockName = getPackageName() + " blockchain sync"; final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName); final WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, lockName); wifiLock.setReferenceCounted(false); application = (WalletApplication) getApplication(); prefs = PreferenceManager.getDefaultSharedPreferences(this); final Wallet wallet = application.getWallet(); final int versionCode = application.applicationVersionCode(); prefs.edit().putInt(Constants.PREFS_KEY_LAST_VERSION, versionCode).commit(); bestChainHeightEver = prefs.getInt(Constants.PREFS_KEY_BEST_CHAIN_HEIGHT_EVER, 0); peerConnectivityListener = new PeerConnectivityListener(); sendBroadcastPeerState(0); final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); registerReceiver(connectivityReceiver, intentFilter); blockChainFile = new File(getDir("blockstore", Context.MODE_WORLD_READABLE | Context.MODE_WORLD_WRITEABLE), Constants.BLOCKCHAIN_FILENAME); final boolean blockChainFileExists = blockChainFile.exists(); if (!blockChainFileExists) { Log.d(TAG, "blockchain does not exist, resetting wallet"); wallet.clearTransactions(0); copyBlockchainSnapshot(blockChainFile); } try { blockStore = new SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile); if (!blockChainFileExists) { // Starting from scratch try { final long earliestKeyCreationTime = wallet.getEarliestKeyCreationTime(); final InputStream checkpointsFileIn = getAssets().open("checkpoints"); CheckpointManager.checkpoint(Constants.NETWORK_PARAMETERS, checkpointsFileIn, blockStore, earliestKeyCreationTime); } catch (IOException e) { Log.d("digitalcoin", "Couldn't find checkpoints file; starting from genesis"); } } blockStore.getChainHead(); // detect corruptions as early as possible } catch (final BlockStoreException x) { blockChainFile.delete(); x.printStackTrace(); throw new Error("blockstore cannot be created", x); } catch (final NullPointerException x) { blockChainFile.delete(); x.printStackTrace(); throw new Error("blockstore cannot be created", x); } try { blockChain = new BlockChain(Constants.NETWORK_PARAMETERS, wallet, blockStore); } catch (final BlockStoreException x) { throw new Error("blockchain cannot be created", x); } application.getWallet().addEventListener(walletEventListener); registerReceiver(tickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK)); } @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { if (BlockchainService.ACTION_CANCEL_COINS_RECEIVED.equals(intent.getAction())) { notificationCount = 0; notificationAccumulatedAmount = BigInteger.ZERO; notificationAddresses.clear(); nm.cancel(NOTIFICATION_ID_COINS_RECEIVED); } if (BlockchainService.ACTION_HOLD_WIFI_LOCK.equals(intent.getAction())) { Log.d(TAG, "acquiring wifilock"); wifiLock.acquire(); } else { Log.d(TAG, "releasing wifilock"); wifiLock.release(); } if (BlockchainService.ACTION_RESET_BLOCKCHAIN.equals(intent.getAction())) { Log.d(TAG, "resetting blockchain"); resetBlockchainOnShutdown = true; stopSelf(); } return START_NOT_STICKY; } private void copyBlockchainSnapshot(final File file) { try { final long t = System.currentTimeMillis(); final String blockchainSnapshotFilename = Constants.BLOCKCHAIN_SNAPSHOT_FILENAME; final InputStream is = getAssets().open(blockchainSnapshotFilename); final OutputStream os = new FileOutputStream(file); Log.i(TAG, "copying blockchain snapshot"); final byte[] buf = new byte[8192]; int read; while (-1 != (read = is.read(buf))) os.write(buf, 0, read); os.close(); is.close(); Log.i(TAG, "finished copying, took " + (System.currentTimeMillis() - t) + " ms"); } catch (final IOException x) { Log.w(TAG, "failed copying, starting from genesis"); file.delete(); } } @Override public void onDestroy() { Log.d(TAG, ".onDestroy()"); unregisterReceiver(tickReceiver); application.getWallet().removeEventListener(walletEventListener); if (peerGroup != null) { peerGroup.removeEventListener(peerConnectivityListener); peerGroup.removeWallet(application.getWallet()); peerGroup.stopAndWait(); Log.i(TAG, "peergroup stopped"); } peerConnectivityListener.stop(); unregisterReceiver(connectivityReceiver); removeBroadcastPeerState(); removeBroadcastBlockchainState(); prefs.edit().putInt(Constants.PREFS_KEY_BEST_CHAIN_HEIGHT_EVER, bestChainHeightEver).commit(); delayHandler.removeCallbacksAndMessages(null); try { blockStore.close(); } catch (final BlockStoreException x) { throw new RuntimeException(x); } application.saveWallet(); if (wakeLock.isHeld()) { Log.d(TAG, "wakelock still held, releasing"); wakeLock.release(); } if (wifiLock.isHeld()) { Log.d(TAG, "wifilock still held, releasing"); wifiLock.release(); } if (resetBlockchainOnShutdown) { Log.d(TAG, "removing blockchain"); blockChainFile.delete(); } super.onDestroy(); } @Override public void onLowMemory() { Log.w(TAG, "low memory detected, stopping service"); stopSelf(); } public void broadcastTransaction(final Transaction tx) { if (peerGroup != null) peerGroup.broadcastTransaction(tx); } public List<Peer> getConnectedPeers() { if (peerGroup != null) return peerGroup.getConnectedPeers(); else return null; } 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; } private void sendBroadcastPeerState(final int numPeers) { final Intent broadcast = new Intent(ACTION_PEER_STATE); broadcast.setPackage(getPackageName()); broadcast.putExtra(ACTION_PEER_STATE_NUM_PEERS, numPeers); sendStickyBroadcast(broadcast); } private void removeBroadcastPeerState() { removeStickyBroadcast(new Intent(ACTION_PEER_STATE)); } private void sendBroadcastBlockchainState(final Date chainDate, final int chainHeight, final boolean replaying, final int download) { final Intent broadcast = new Intent(ACTION_BLOCKCHAIN_STATE); broadcast.setPackage(getPackageName()); broadcast.putExtra(ACTION_BLOCKCHAIN_STATE_BEST_CHAIN_DATE, chainDate); broadcast.putExtra(ACTION_BLOCKCHAIN_STATE_BEST_CHAIN_HEIGHT, chainHeight); broadcast.putExtra(ACTION_BLOCKCHAIN_STATE_REPLAYING, replaying); broadcast.putExtra(ACTION_BLOCKCHAIN_STATE_DOWNLOAD, download); sendStickyBroadcast(broadcast); } private void removeBroadcastBlockchainState() { removeStickyBroadcast(new Intent(ACTION_BLOCKCHAIN_STATE)); } public void notifyWidgets() { final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); final ComponentName providerName = new ComponentName(this, WalletBalanceWidgetProvider.class); final int[] appWidgetIds; try { appWidgetIds = appWidgetManager.getAppWidgetIds(providerName); } catch(RuntimeException e) { // Bug #6 - App server dead? Log.e("digitalcoin", "App server appears dead - Runtime Exception when running getAppWidgetIds. Returning.."); return; } if (appWidgetIds.length > 0) { final Wallet wallet = application.getWallet(); final BigInteger balance = wallet.getBalance(BalanceType.ESTIMATED); WalletBalanceWidgetProvider.updateWidgets(this, appWidgetManager, appWidgetIds, balance); } } }