Skip to content

Commit

Permalink
play sounds on notifications #1284
Browse files Browse the repository at this point in the history
  • Loading branch information
woodser committed Sep 30, 2024
1 parent b2a6708 commit b940021
Show file tree
Hide file tree
Showing 29 changed files with 173 additions and 16 deletions.
15 changes: 14 additions & 1 deletion core/src/main/java/haveno/core/api/CoreNotificationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import com.google.inject.Singleton;
import haveno.core.api.model.TradeInfo;
import haveno.core.support.messages.ChatMessage;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.SellerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.Trade.Phase;
import haveno.proto.grpc.NotificationMessage;
import haveno.proto.grpc.NotificationMessage.NotificationType;
import java.util.Iterator;
Expand Down Expand Up @@ -46,7 +50,15 @@ public void sendAppInitializedNotification() {
.build());
}

public void sendTradeNotification(Trade trade, String title, String message) {
public void sendTradeNotification(Trade trade, Phase phase, String title, String message) {

// play chime when maker's trade is taken
if (trade instanceof MakerTrade && phase == Trade.Phase.DEPOSITS_PUBLISHED) HavenoUtils.playChimeSound();

// play chime when seller sees buyer confirm payment sent
if (trade instanceof SellerTrade && phase == Trade.Phase.PAYMENT_SENT) HavenoUtils.playChimeSound();

// send notification
sendNotification(NotificationMessage.newBuilder()
.setType(NotificationType.TRADE_UPDATE)
.setTrade(TradeInfo.toTradeInfo(trade).toProtoMessage())
Expand All @@ -57,6 +69,7 @@ public void sendTradeNotification(Trade trade, String title, String message) {
}

public void sendChatNotification(ChatMessage chatMessage) {
HavenoUtils.playChimeSound();
sendNotification(NotificationMessage.newBuilder()
.setType(NotificationType.CHAT_MESSAGE)
.setTimestamp(System.currentTimeMillis())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public static XmrBalanceInfo fromProto(haveno.proto.grpc.XmrBalanceInfo proto) {
public String toString() {
return "XmrBalanceInfo{" +
"balance=" + balance +
"unlockedBalance=" + availableBalance +
", unlockedBalance=" + availableBalance +
", lockedBalance=" + pendingBalance +
", reservedOfferBalance=" + reservedOfferBalance +
", reservedTradeBalance=" + reservedTradeBalance +
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/java/haveno/core/app/HavenoSetup.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import haveno.core.alert.AlertManager;
import haveno.core.alert.PrivateNotificationManager;
import haveno.core.alert.PrivateNotificationPayload;
import haveno.core.api.CoreContext;
import haveno.core.api.XmrConnectionService;
import haveno.core.api.XmrLocalNode;
import haveno.core.locale.Res;
Expand Down Expand Up @@ -131,7 +132,10 @@ public class HavenoSetup {
private final Preferences preferences;
private final User user;
private final AlertManager alertManager;
@Getter
private final Config config;
@Getter
private final CoreContext coreContext;
private final AccountAgeWitnessService accountAgeWitnessService;
private final TorSetup torSetup;
private final CoinFormatter formatter;
Expand Down Expand Up @@ -228,6 +232,7 @@ public HavenoSetup(DomainInitialisation domainInitialisation,
User user,
AlertManager alertManager,
Config config,
CoreContext coreContext,
AccountAgeWitnessService accountAgeWitnessService,
TorSetup torSetup,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
Expand All @@ -253,6 +258,7 @@ public HavenoSetup(DomainInitialisation domainInitialisation,
this.user = user;
this.alertManager = alertManager;
this.config = config;
this.coreContext = coreContext;
this.accountAgeWitnessService = accountAgeWitnessService;
this.torSetup = torSetup;
this.formatter = formatter;
Expand All @@ -263,6 +269,7 @@ public HavenoSetup(DomainInitialisation domainInitialisation,
this.arbitrationManager = arbitrationManager;

HavenoUtils.havenoSetup = this;
HavenoUtils.preferences = preferences;
}

///////////////////////////////////////////////////////////////////////////////////////////
Expand Down
75 changes: 75 additions & 0 deletions core/src/main/java/haveno/core/trade/HavenoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing;
import haveno.common.crypto.Sig;
import haveno.common.file.FileUtil;
import haveno.common.util.Utilities;
import haveno.core.api.CoreNotificationService;
import haveno.core.api.XmrConnectionService;
import haveno.core.app.HavenoSetup;
import haveno.core.offer.OfferPayload;
Expand All @@ -36,9 +38,12 @@
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.trade.messages.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.user.Preferences;
import haveno.core.util.JsonUtil;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.NodeAddress;

import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
Expand All @@ -53,6 +58,13 @@
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;

import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroOutput;
Expand Down Expand Up @@ -110,11 +122,18 @@ public static Object getWalletFunctionLock() {
public static XmrWalletService xmrWalletService;
public static XmrConnectionService xmrConnectionService;
public static OpenOfferManager openOfferManager;
public static CoreNotificationService notificationService;
public static Preferences preferences;

public static boolean isSeedNode() {
return havenoSetup == null;
}

public static boolean isDaemon() {
if (isSeedNode()) return true;
return havenoSetup.getCoreContext().isApiUser();
}

@SuppressWarnings("unused")
public static Date getReleaseDate() {
if (RELEASE_DATE == null) return null;
Expand Down Expand Up @@ -533,4 +552,60 @@ public static boolean isTransactionRejected(Throwable e) {
public static boolean isIllegal(Throwable e) {
return e instanceof IllegalArgumentException || e instanceof IllegalStateException;
}

public static void playChimeSound() {
playAudioFile("chime.wav");
}

public static void playCashRegisterSound() {
playAudioFile("cash_register.wav");
}

private static void playAudioFile(String fileName) {
if (isDaemon()) return; // ignore if running as daemon
if (!preferences.getUseSoundForNotificationsProperty().get()) return; // ignore if sounds disabled
new Thread(() -> {
try {

// get audio file
File wavFile = new File(havenoSetup.getConfig().appDataDir, fileName);
if (!wavFile.exists()) FileUtil.resourceToFile(fileName, wavFile);
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(wavFile);

// get original format
AudioFormat baseFormat = audioInputStream.getFormat();

// set target format: PCM_SIGNED, 16-bit
AudioFormat targetFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
baseFormat.getSampleRate(),
16, // 16-bit instead of 32-bit float
baseFormat.getChannels(),
baseFormat.getChannels() * 2, // Frame size: 2 bytes per channel (16-bit)
baseFormat.getSampleRate(),
false // Little-endian
);

// convert audio to target format
AudioInputStream convertedStream = AudioSystem.getAudioInputStream(targetFormat, audioInputStream);

// play audio
DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat);
SourceDataLine sourceLine = (SourceDataLine) AudioSystem.getLine(info);
sourceLine.open(targetFormat);
sourceLine.start();
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = convertedStream.read(buffer, 0, buffer.length)) != -1) {
sourceLine.write(buffer, 0, bytesRead);
}
sourceLine.drain();
sourceLine.close();
convertedStream.close();
audioInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
8 changes: 8 additions & 0 deletions core/src/main/java/haveno/core/trade/Trade.java
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ public void initialize(ProcessModelServiceProvider serviceProvider) {
ThreadUtils.submitToPool(() -> {
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) onDepositsPublished();
if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent();
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
if (isPaymentReceived()) {
UserThread.execute(() -> {
Expand Down Expand Up @@ -2833,6 +2834,7 @@ private void onDepositsPublished() {
// close open offer or reset address entries
if (this instanceof MakerTrade) {
processModel.getOpenOfferManager().closeOpenOffer(getOffer());
HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_PUBLISHED, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation
} else {
getXmrWalletService().resetAddressEntriesForOpenOffer(getId());
}
Expand All @@ -2841,6 +2843,12 @@ private void onDepositsPublished() {
ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages()));
}

private void onPaymentSent() {
if (this instanceof SellerTrade) {
HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); // TODO (woodser): use language translation
}
}

///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
Expand Down
14 changes: 3 additions & 11 deletions core/src/main/java/haveno/core/trade/TradeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@
import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.trade.Trade.DisputeState;
import haveno.core.trade.Trade.Phase;
import haveno.core.trade.failed.FailedTradesManager;
import haveno.core.trade.handlers.TradeResultHandler;
import haveno.core.trade.messages.DepositRequest;
Expand Down Expand Up @@ -134,7 +133,6 @@
import monero.daemon.model.MoneroTx;
import org.bitcoinj.core.Coin;
import org.bouncycastle.crypto.params.KeyParameter;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -258,7 +256,9 @@ public TradeManager(User user,

failedTradesManager.setUnFailTradeCallback(this::unFailTrade);

xmrWalletService.setTradeManager(this);
// TODO: better way to set references
xmrWalletService.setTradeManager(this); // TODO: set reference in HavenoUtils for consistency
HavenoUtils.notificationService = notificationService;
}


Expand Down Expand Up @@ -599,14 +599,6 @@ private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender
initTradeAndProtocol(trade, createTradeProtocol(trade));
addTrade(trade);

// notify on phase changes
// TODO (woodser): save subscription, bind on startup
EasyBind.subscribe(trade.statePhaseProperty(), phase -> {
if (phase == Phase.DEPOSITS_PUBLISHED) {
notificationService.sendTradeNotification(trade, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation
}
});

// process with protocol
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
log.warn("Maker error during trade initialization: " + errorMessage);
Expand Down
14 changes: 14 additions & 0 deletions core/src/main/java/haveno/core/user/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public boolean isUseTorForXmr() {
private final String xmrNodesFromOptions;
@Getter
private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode());
@Getter
private final BooleanProperty useSoundForNotificationsProperty = new SimpleBooleanProperty(prefPayload.isUseSoundForNotifications());

///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
Expand Down Expand Up @@ -162,6 +164,11 @@ public Preferences(PersistenceManager<PreferencesPayload> persistenceManager,
requestPersistence();
});

useSoundForNotificationsProperty.addListener((ov) -> {
prefPayload.setUseSoundForNotifications(useSoundForNotificationsProperty.get());
requestPersistence();
});

traditionalCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> {
prefPayload.getTraditionalCurrencies().clear();
prefPayload.getTraditionalCurrencies().addAll(traditionalCurrenciesAsObservable);
Expand Down Expand Up @@ -259,6 +266,7 @@ private void setupPreferences() {
// set all properties
useAnimationsProperty.set(prefPayload.isUseAnimations());
useStandbyModeProperty.set(prefPayload.isUseStandbyMode());
useSoundForNotificationsProperty.set(prefPayload.isUseSoundForNotifications());
cssThemeProperty.set(prefPayload.getCssTheme());


Expand Down Expand Up @@ -697,6 +705,10 @@ public void setUseStandbyMode(boolean useStandbyMode) {
this.useStandbyModeProperty.set(useStandbyMode);
}

public void setUseSoundForNotifications(boolean useSoundForNotifications) {
this.useSoundForNotificationsProperty.set(useSoundForNotifications);
}

public void setTakeOfferSelectedPaymentAccountId(String value) {
prefPayload.setTakeOfferSelectedPaymentAccountId(value);
requestPersistence();
Expand Down Expand Up @@ -946,6 +958,8 @@ private interface ExcludesDelegateMethods {

void setUseStandbyMode(boolean useStandbyMode);

void setUseSoundForNotifications(boolean useSoundForNotifications);

void setTakeOfferSelectedPaymentAccountId(String value);

void setIgnoreDustThreshold(int value);
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/haveno/core/user/PreferencesPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
private boolean useMarketNotifications = true;
private boolean usePriceNotifications = true;
private boolean useStandbyMode = false;
private boolean useSoundForNotifications = true;
@Nullable
private String rpcUser;
@Nullable
Expand Down Expand Up @@ -185,6 +186,7 @@ public Message toProtoMessage() {
.setUseMarketNotifications(useMarketNotifications)
.setUsePriceNotifications(usePriceNotifications)
.setUseStandbyMode(useStandbyMode)
.setUseSoundForNotifications(useSoundForNotifications)
.setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent)
.setIgnoreDustThreshold(ignoreDustThreshold)
.setClearDataAfterDays(clearDataAfterDays)
Expand Down Expand Up @@ -280,6 +282,7 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co
proto.getUseMarketNotifications(),
proto.getUsePriceNotifications(),
proto.getUseStandbyMode(),
proto.getUseSoundForNotifications(),
proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(),
proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(),
proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(),
Expand Down
23 changes: 22 additions & 1 deletion core/src/main/java/haveno/core/xmr/Balances.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public void onBalanceChanged(BigInteger balance) {

public XmrBalanceInfo getBalances() {
synchronized (this) {
if (availableBalance == null) return null;
return new XmrBalanceInfo(availableBalance.longValue() + pendingBalance.longValue(),
availableBalance.longValue(),
pendingBalance.longValue(),
Expand All @@ -127,6 +128,9 @@ private void doUpdateBalances() {
synchronized (this) {
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {

// get non-trade balance before
BigInteger balanceSumBefore = getNonTradeBalanceSum();

// get wallet balances
BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance();
availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance();
Expand Down Expand Up @@ -160,8 +164,25 @@ private void doUpdateBalances() {
reservedBalance = reservedOfferBalance.add(reservedTradeBalance);

// notify balance update
UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1));
UserThread.execute(() -> {

// check if funds received
boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0;
if (fundsReceived) {
HavenoUtils.playCashRegisterSound();
}

// increase counter to notify listeners
updateCounter.set(updateCounter.get() + 1);
});
}
}
}

private BigInteger getNonTradeBalanceSum() {
synchronized (this) {
if (availableBalance == null) return null;
return availableBalance.add(pendingBalance).add(reservedOfferBalance);
}
}
}
Binary file added core/src/main/resources/cash_register.wav
Binary file not shown.
Binary file added core/src/main/resources/chime.wav
Binary file not shown.
Loading

0 comments on commit b940021

Please sign in to comment.