package com.mygeopay.wallet.service;
import android.app.NotificationManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import android.text.format.DateUtils;
import com.mygeopay.core.network.ConnectivityHelper;
import com.mygeopay.core.network.ServerClients;
import com.mygeopay.core.wallet.Wallet;
import com.mygeopay.core.wallet.WalletAccount;
import com.mygeopay.wallet.Configuration;
import com.mygeopay.wallet.Constants;
import com.mygeopay.wallet.WalletApplication;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.CheckForNull;
/**
* @author John L. Jegutanis
* @author Andreas Schildbach
*/
public class CoinServiceImpl extends Service implements CoinService {
private WalletApplication application;
private Configuration config;
private ConnectivityManager connManager;
private ConnectivityHelper connHelper;
@CheckForNull
private ServerClients clients;
private String lastAccount;
// private PowerManager.WakeLock wakeLock;
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 AtomicInteger transactionsReceived = new AtomicInteger();
private long serviceCreatedAt;
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 APPWIDGET_THROTTLE_MS = DateUtils.SECOND_IN_MILLIS;
private static final Logger log = LoggerFactory.getLogger(CoinService.class);
// private final WalletEventListener walletEventListener = new ThrottlingWalletChangeListener(APPWIDGET_THROTTLE_MS)
// {
// @Override
// public void onThrottledWalletChanged()
// {
// notifyWidgets();
// }
//
// @Override
// public void onCoinsReceived(final Wallet wallet, final Transaction tx, final BigInteger prevBalance, final BigInteger newBalance)
// {
// transactionsReceived.incrementAndGet();
//
// final int bestChainHeight = blockChain.getBestChainHeight();
//
// final Address from = WalletUtils.getFirstFromAddress(tx);
// final BigInteger amount = tx.getValue(wallet);
// final TransactionConfidence.ConfidenceType confidenceType = tx.getConfidence().getConfidenceType();
//
// handler.post(new Runnable()
// {
// @Override
// public void run()
// {
// final boolean isReceived = amount.signum() > 0;
// final boolean replaying = bestChainHeight < bestChainHeightEver;
// final boolean isReplayedTx = confidenceType == TransactionConfidence.ConfidenceType.BUILDING && replaying;
//
// if (isReceived && !isReplayedTx)
// notifyCoinsReceived(from, amount);
// }
// });
// }
//
// @Override
// public void onCoinsSent(final Wallet wallet, final Transaction tx, final BigInteger prevBalance, final BigInteger newBalance)
// {
// transactionsReceived.incrementAndGet();
// }
// };
// private void notifyCoinsReceived(@Nullable final Address from, @Nonnull 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 btcPrecision = config.getBtcPrecision();
// final int btcShift = config.getBtcShift();
// final String btcPrefix = config.getBtcPrefix();
//
// final String packageFlavor = application.applicationPackageFlavor();
// final String msgSuffix = packageFlavor != null ? " [" + packageFlavor + "]" : "";
//
// final String tickerMsg = getString(R.string.notification_coins_received_msg,
// btcPrefix + ' ' + GenericUtils.formatCoinValue(amount, btcPrecision, btcShift))
// + msgSuffix;
//
// final String msg = getString(R.string.notification_coins_received_msg,
// btcPrefix + ' ' + GenericUtils.formatCoinValue(notificationAccumulatedAmount, btcPrecision, btcShift))
// + msgSuffix;
//
// final StringBuilder text = new StringBuilder();
// for (final Address address : notificationAddresses)
// {
// if (text.length() > 0)
// text.append(", ");
//
// final String addressStr = address.toString();
// final String label = AddressBookProvider.resolveLabel(getApplicationContext(), addressStr);
// text.append(label != null ? label : addressStr);
// }
//
// final NotificationCompat.Builder notification = new NotificationCompat.Builder(this);
// notification.setSmallIcon(R.drawable.stat_notify_received);
// notification.setTicker(tickerMsg);
// notification.setContentTitle(msg);
// if (text.length() > 0)
// 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 final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() {
private boolean hasConnectivity;
private boolean hasStorage = true;
private int currentNetworkType = -1;
@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);
NetworkInfo activeInfo = connManager.getActiveNetworkInfo();
boolean isNetworkChanged;
if (activeInfo != null && activeInfo.isConnected()) {
isNetworkChanged = currentNetworkType != activeInfo.getType();
currentNetworkType = activeInfo.getType();
} else {
isNetworkChanged = false;
currentNetworkType = -1;
}
log.info("network is " + (hasConnectivity ? "up" : "down"));
log.info("network type " + (isNetworkChanged ? "changed" : "didn't change"));
check(isNetworkChanged);
} else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
hasStorage = false;
log.info("device storage low");
check(false);
} else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
hasStorage = true;
log.info("device storage ok");
check(false);
}
}
// @SuppressLint("Wakelock")
private void check(boolean isNetworkChanged) {
Wallet wallet = application.getWallet();
final boolean hasEverything = hasConnectivity && hasStorage && (wallet != null);
if (hasEverything && clients == null) {
// log.debug("acquiring wakelock");
// wakeLock.acquire();
log.info("Creating coins clients");
clients = getServerClients(wallet);
if (lastAccount != null) clients.startAsync(wallet.getAccount(lastAccount));
} else if (hasEverything && isNetworkChanged) {
log.info("Restarting coins clients as network changed");
clients.resetConnections();
} else if (!hasEverything && clients != null) {
log.info("stopping stratum clients");
clients.stopAllAsync();
clients = null;
// log.debug("releasing wakelock");
// wakeLock.release();
}
}
};
private ServerClients getServerClients(Wallet wallet) {
return new ServerClients(Constants.DEFAULT_COINS_SERVERS, wallet, connHelper);
}
private final BroadcastReceiver tickReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
log.debug("Received a tick {}", intent);
if (clients != null) {
clients.ping();
}
long lastStop = application.getLastStop();
if (lastStop > 0) {
long secondsIdle = (SystemClock.elapsedRealtime() - lastStop) / 1000;
if (secondsIdle > Constants.STOP_SERVICE_AFTER_IDLE_SECS) {
log.info("Idling detected, stopping service");
stopSelf();
}
}
}
};
public class LocalBinder extends Binder {
public CoinService getService() {
return CoinServiceImpl.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 String lockName = getPackageName() + " blockchain sync";
final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
// wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lockName);
application = (WalletApplication) getApplication();
config = application.getConfiguration();
connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
connHelper = getConnectivityHelper(connManager);
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);
registerReceiver(tickReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
}
private ConnectivityHelper getConnectivityHelper(final ConnectivityManager manager) {
return new ConnectivityHelper() {
@Override
public boolean isConnected() {
NetworkInfo activeInfo = manager.getActiveNetworkInfo();
return activeInfo != null && activeInfo.isConnected();
}
};
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId)
{
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 (CoinService.ACTION_CANCEL_COINS_RECEIVED.equals(action)) {
notificationCount = 0;
notificationAccumulatedAmount = BigInteger.ZERO;
notificationAddresses.clear();
nm.cancel(NOTIFICATION_ID_COINS_RECEIVED);
} else if (CoinService.ACTION_RESET_WALLET.equals(action)) {
if (application.getWallet() != null) {
Wallet wallet = application.getWallet();
if (intent.hasExtra(Constants.ARG_ACCOUNT_ID)) {
String accountId = intent.getStringExtra(Constants.ARG_ACCOUNT_ID);
WalletAccount pocket = wallet.getAccount(accountId);
if (pocket != null) {
WalletAccount account = wallet.refresh(accountId);
if (clients != null && account != null) {
clients.resetAccount(account);
}
} else {
log.warn("Tried to start a service for account id {} but no pocket found.",
accountId);
}
} else {
log.warn("Missing account id argument, not doing anything");
}
} else {
log.warn("Got wallet reset intent, but no wallet is available");
}
} else if (CoinService.ACTION_CONNECT_COIN.equals(action)) {
if (application.getWallet() != null) {
Wallet wallet = application.getWallet();
if (intent.hasExtra(Constants.ARG_ACCOUNT_ID)) {
lastAccount = intent.getStringExtra(Constants.ARG_ACCOUNT_ID);
WalletAccount pocket = wallet.getAccount(lastAccount);
if (pocket != null) {
if (clients == null && connHelper.isConnected()) {
clients = getServerClients(wallet);
}
if (clients != null) {
clients.startAsync(pocket);
}
} else {
log.warn("Tried to start a service for account id {} but no pocket found.",
lastAccount);
}
} else {
log.warn("Missing account id argument, not doing anything");
}
} else {
log.error("Got connect coin intent, but no wallet is available");
}
} else if (CoinService.ACTION_BROADCAST_TRANSACTION.equals(action)) {
final Sha256Hash hash = new Sha256Hash(intent.getByteArrayExtra(CoinService.ACTION_BROADCAST_TRANSACTION_HASH));
final Transaction tx = null; // FIXME
if (clients != null)
{
log.info("broadcasting transaction " + tx == null ? null : tx.getHashAsString());
broadcastTransaction(tx);
}
else
{
log.info("client not available, not broadcasting transaction " + tx == null ? null : tx.getHashAsString());
}
}
return START_REDELIVER_INTENT;
}
private void broadcastTransaction(Transaction tx) {
// TODO send broadcast message
}
@Override
public void onDestroy()
{
log.debug(".onDestroy()");
unregisterReceiver(tickReceiver);
unregisterReceiver(connectivityReceiver);
if (clients != null) {
clients.stopAllAsync();
clients = null;
}
application.saveWalletNow();
// if (wakeLock.isHeld())
// {
// log.debug("wakelock still held, releasing");
// wakeLock.release();
// }
super.onDestroy();
log.info("service was up for " + ((System.currentTimeMillis() - serviceCreatedAt) / 1000 / 60) + " minutes");
}
@Override
public void onLowMemory() {
log.warn("low memory detected, stopping service");
stopSelf();
}
}