Skip to content

Commit f2f25f6

Browse files
committed
play sounds on notifications #1284
1 parent 3ffda0f commit f2f25f6

29 files changed

+173
-16
lines changed

core/src/main/java/haveno/core/api/CoreNotificationService.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import com.google.inject.Singleton;
44
import haveno.core.api.model.TradeInfo;
55
import haveno.core.support.messages.ChatMessage;
6+
import haveno.core.trade.HavenoUtils;
7+
import haveno.core.trade.MakerTrade;
8+
import haveno.core.trade.SellerTrade;
69
import haveno.core.trade.Trade;
10+
import haveno.core.trade.Trade.Phase;
711
import haveno.proto.grpc.NotificationMessage;
812
import haveno.proto.grpc.NotificationMessage.NotificationType;
913
import java.util.Iterator;
@@ -46,7 +50,15 @@ public void sendAppInitializedNotification() {
4650
.build());
4751
}
4852

49-
public void sendTradeNotification(Trade trade, String title, String message) {
53+
public void sendTradeNotification(Trade trade, Phase phase, String title, String message) {
54+
55+
// play chime when maker's trade is taken
56+
if (trade instanceof MakerTrade && phase == Trade.Phase.DEPOSITS_PUBLISHED) HavenoUtils.playChimeSound();
57+
58+
// play chime when seller sees buyer confirm payment sent
59+
if (trade instanceof SellerTrade && phase == Trade.Phase.PAYMENT_SENT) HavenoUtils.playChimeSound();
60+
61+
// send notification
5062
sendNotification(NotificationMessage.newBuilder()
5163
.setType(NotificationType.TRADE_UPDATE)
5264
.setTrade(TradeInfo.toTradeInfo(trade).toProtoMessage())
@@ -57,6 +69,7 @@ public void sendTradeNotification(Trade trade, String title, String message) {
5769
}
5870

5971
public void sendChatNotification(ChatMessage chatMessage) {
72+
HavenoUtils.playChimeSound();
6073
sendNotification(NotificationMessage.newBuilder()
6174
.setType(NotificationType.CHAT_MESSAGE)
6275
.setTimestamp(System.currentTimeMillis())

core/src/main/java/haveno/core/api/model/XmrBalanceInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public static XmrBalanceInfo fromProto(haveno.proto.grpc.XmrBalanceInfo proto) {
9898
public String toString() {
9999
return "XmrBalanceInfo{" +
100100
"balance=" + balance +
101-
"unlockedBalance=" + availableBalance +
101+
", unlockedBalance=" + availableBalance +
102102
", lockedBalance=" + pendingBalance +
103103
", reservedOfferBalance=" + reservedOfferBalance +
104104
", reservedTradeBalance=" + reservedTradeBalance +

core/src/main/java/haveno/core/app/HavenoSetup.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import haveno.core.alert.AlertManager;
5454
import haveno.core.alert.PrivateNotificationManager;
5555
import haveno.core.alert.PrivateNotificationPayload;
56+
import haveno.core.api.CoreContext;
5657
import haveno.core.api.XmrConnectionService;
5758
import haveno.core.api.XmrLocalNode;
5859
import haveno.core.locale.Res;
@@ -131,7 +132,10 @@ public class HavenoSetup {
131132
private final Preferences preferences;
132133
private final User user;
133134
private final AlertManager alertManager;
135+
@Getter
134136
private final Config config;
137+
@Getter
138+
private final CoreContext coreContext;
135139
private final AccountAgeWitnessService accountAgeWitnessService;
136140
private final TorSetup torSetup;
137141
private final CoinFormatter formatter;
@@ -228,6 +232,7 @@ public HavenoSetup(DomainInitialisation domainInitialisation,
228232
User user,
229233
AlertManager alertManager,
230234
Config config,
235+
CoreContext coreContext,
231236
AccountAgeWitnessService accountAgeWitnessService,
232237
TorSetup torSetup,
233238
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
@@ -253,6 +258,7 @@ public HavenoSetup(DomainInitialisation domainInitialisation,
253258
this.user = user;
254259
this.alertManager = alertManager;
255260
this.config = config;
261+
this.coreContext = coreContext;
256262
this.accountAgeWitnessService = accountAgeWitnessService;
257263
this.torSetup = torSetup;
258264
this.formatter = formatter;
@@ -263,6 +269,7 @@ public HavenoSetup(DomainInitialisation domainInitialisation,
263269
this.arbitrationManager = arbitrationManager;
264270

265271
HavenoUtils.havenoSetup = this;
272+
HavenoUtils.preferences = preferences;
266273
}
267274

268275
///////////////////////////////////////////////////////////////////////////////////////////

core/src/main/java/haveno/core/trade/HavenoUtils.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
import haveno.common.crypto.KeyRing;
2828
import haveno.common.crypto.PubKeyRing;
2929
import haveno.common.crypto.Sig;
30+
import haveno.common.file.FileUtil;
3031
import haveno.common.util.Utilities;
32+
import haveno.core.api.CoreNotificationService;
3133
import haveno.core.api.XmrConnectionService;
3234
import haveno.core.app.HavenoSetup;
3335
import haveno.core.offer.OfferPayload;
@@ -36,9 +38,12 @@
3638
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
3739
import haveno.core.trade.messages.PaymentReceivedMessage;
3840
import haveno.core.trade.messages.PaymentSentMessage;
41+
import haveno.core.user.Preferences;
3942
import haveno.core.util.JsonUtil;
4043
import haveno.core.xmr.wallet.XmrWalletService;
4144
import haveno.network.p2p.NodeAddress;
45+
46+
import java.io.File;
4247
import java.math.BigDecimal;
4348
import java.math.BigInteger;
4449
import java.net.InetAddress;
@@ -53,6 +58,13 @@
5358
import java.util.List;
5459
import java.util.Locale;
5560
import java.util.concurrent.CountDownLatch;
61+
62+
import javax.sound.sampled.AudioFormat;
63+
import javax.sound.sampled.AudioInputStream;
64+
import javax.sound.sampled.AudioSystem;
65+
import javax.sound.sampled.DataLine;
66+
import javax.sound.sampled.SourceDataLine;
67+
5668
import lombok.extern.slf4j.Slf4j;
5769
import monero.common.MoneroRpcConnection;
5870
import monero.daemon.model.MoneroOutput;
@@ -110,11 +122,18 @@ public static Object getWalletFunctionLock() {
110122
public static XmrWalletService xmrWalletService;
111123
public static XmrConnectionService xmrConnectionService;
112124
public static OpenOfferManager openOfferManager;
125+
public static CoreNotificationService notificationService;
126+
public static Preferences preferences;
113127

114128
public static boolean isSeedNode() {
115129
return havenoSetup == null;
116130
}
117131

132+
public static boolean isDaemon() {
133+
if (isSeedNode()) return true;
134+
return havenoSetup.getCoreContext().isApiUser();
135+
}
136+
118137
@SuppressWarnings("unused")
119138
public static Date getReleaseDate() {
120139
if (RELEASE_DATE == null) return null;
@@ -533,4 +552,60 @@ public static boolean isTransactionRejected(Throwable e) {
533552
public static boolean isIllegal(Throwable e) {
534553
return e instanceof IllegalArgumentException || e instanceof IllegalStateException;
535554
}
555+
556+
public static void playChimeSound() {
557+
playAudioFile("chime.wav");
558+
}
559+
560+
public static void playCashRegisterSound() {
561+
playAudioFile("cash_register.wav");
562+
}
563+
564+
private static void playAudioFile(String fileName) {
565+
if (isDaemon()) return; // ignore if running as daemon
566+
if (!preferences.getUseSoundForNotificationsProperty().get()) return; // ignore if sounds disabled
567+
new Thread(() -> {
568+
try {
569+
570+
// get audio file
571+
File wavFile = new File(havenoSetup.getConfig().appDataDir, fileName);
572+
if (!wavFile.exists()) FileUtil.resourceToFile(fileName, wavFile);
573+
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(wavFile);
574+
575+
// get original format
576+
AudioFormat baseFormat = audioInputStream.getFormat();
577+
578+
// set target format: PCM_SIGNED, 16-bit
579+
AudioFormat targetFormat = new AudioFormat(
580+
AudioFormat.Encoding.PCM_SIGNED,
581+
baseFormat.getSampleRate(),
582+
16, // 16-bit instead of 32-bit float
583+
baseFormat.getChannels(),
584+
baseFormat.getChannels() * 2, // Frame size: 2 bytes per channel (16-bit)
585+
baseFormat.getSampleRate(),
586+
false // Little-endian
587+
);
588+
589+
// convert audio to target format
590+
AudioInputStream convertedStream = AudioSystem.getAudioInputStream(targetFormat, audioInputStream);
591+
592+
// play audio
593+
DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat);
594+
SourceDataLine sourceLine = (SourceDataLine) AudioSystem.getLine(info);
595+
sourceLine.open(targetFormat);
596+
sourceLine.start();
597+
byte[] buffer = new byte[1024];
598+
int bytesRead = 0;
599+
while ((bytesRead = convertedStream.read(buffer, 0, buffer.length)) != -1) {
600+
sourceLine.write(buffer, 0, bytesRead);
601+
}
602+
sourceLine.drain();
603+
sourceLine.close();
604+
convertedStream.close();
605+
audioInputStream.close();
606+
} catch (Exception e) {
607+
e.printStackTrace();
608+
}
609+
}).start();
610+
}
536611
}

core/src/main/java/haveno/core/trade/Trade.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@ public void initialize(ProcessModelServiceProvider serviceProvider) {
649649
ThreadUtils.submitToPool(() -> {
650650
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
651651
if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) onDepositsPublished();
652+
if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent();
652653
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
653654
if (isPaymentReceived()) {
654655
UserThread.execute(() -> {
@@ -2833,6 +2834,7 @@ private void onDepositsPublished() {
28332834
// close open offer or reset address entries
28342835
if (this instanceof MakerTrade) {
28352836
processModel.getOpenOfferManager().closeOpenOffer(getOffer());
2837+
HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_PUBLISHED, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation
28362838
} else {
28372839
getXmrWalletService().resetAddressEntriesForOpenOffer(getId());
28382840
}
@@ -2841,6 +2843,12 @@ private void onDepositsPublished() {
28412843
ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages()));
28422844
}
28432845

2846+
private void onPaymentSent() {
2847+
if (this instanceof SellerTrade) {
2848+
HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); // TODO (woodser): use language translation
2849+
}
2850+
}
2851+
28442852
///////////////////////////////////////////////////////////////////////////////////////////
28452853
// PROTO BUFFER
28462854
///////////////////////////////////////////////////////////////////////////////////////////

core/src/main/java/haveno/core/trade/TradeManager.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
import haveno.core.support.dispute.messages.DisputeClosedMessage;
6969
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
7070
import haveno.core.trade.Trade.DisputeState;
71-
import haveno.core.trade.Trade.Phase;
7271
import haveno.core.trade.failed.FailedTradesManager;
7372
import haveno.core.trade.handlers.TradeResultHandler;
7473
import haveno.core.trade.messages.DepositRequest;
@@ -134,7 +133,6 @@
134133
import monero.daemon.model.MoneroTx;
135134
import org.bitcoinj.core.Coin;
136135
import org.bouncycastle.crypto.params.KeyParameter;
137-
import org.fxmisc.easybind.EasyBind;
138136
import org.slf4j.Logger;
139137
import org.slf4j.LoggerFactory;
140138

@@ -258,7 +256,9 @@ public TradeManager(User user,
258256

259257
failedTradesManager.setUnFailTradeCallback(this::unFailTrade);
260258

261-
xmrWalletService.setTradeManager(this);
259+
// TODO: better way to set references
260+
xmrWalletService.setTradeManager(this); // TODO: set reference in HavenoUtils for consistency
261+
HavenoUtils.notificationService = notificationService;
262262
}
263263

264264

@@ -599,14 +599,6 @@ private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender
599599
initTradeAndProtocol(trade, createTradeProtocol(trade));
600600
addTrade(trade);
601601

602-
// notify on phase changes
603-
// TODO (woodser): save subscription, bind on startup
604-
EasyBind.subscribe(trade.statePhaseProperty(), phase -> {
605-
if (phase == Phase.DEPOSITS_PUBLISHED) {
606-
notificationService.sendTradeNotification(trade, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation
607-
}
608-
});
609-
610602
// process with protocol
611603
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
612604
log.warn("Maker error during trade initialization: " + errorMessage);

core/src/main/java/haveno/core/user/Preferences.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ public boolean isUseTorForXmr() {
132132
private final String xmrNodesFromOptions;
133133
@Getter
134134
private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode());
135+
@Getter
136+
private final BooleanProperty useSoundForNotificationsProperty = new SimpleBooleanProperty(prefPayload.isUseSoundForNotifications());
135137

136138
///////////////////////////////////////////////////////////////////////////////////////////
137139
// Constructor
@@ -162,6 +164,11 @@ public Preferences(PersistenceManager<PreferencesPayload> persistenceManager,
162164
requestPersistence();
163165
});
164166

167+
useSoundForNotificationsProperty.addListener((ov) -> {
168+
prefPayload.setUseSoundForNotifications(useSoundForNotificationsProperty.get());
169+
requestPersistence();
170+
});
171+
165172
traditionalCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> {
166173
prefPayload.getTraditionalCurrencies().clear();
167174
prefPayload.getTraditionalCurrencies().addAll(traditionalCurrenciesAsObservable);
@@ -259,6 +266,7 @@ private void setupPreferences() {
259266
// set all properties
260267
useAnimationsProperty.set(prefPayload.isUseAnimations());
261268
useStandbyModeProperty.set(prefPayload.isUseStandbyMode());
269+
useSoundForNotificationsProperty.set(prefPayload.isUseSoundForNotifications());
262270
cssThemeProperty.set(prefPayload.getCssTheme());
263271

264272

@@ -697,6 +705,10 @@ public void setUseStandbyMode(boolean useStandbyMode) {
697705
this.useStandbyModeProperty.set(useStandbyMode);
698706
}
699707

708+
public void setUseSoundForNotifications(boolean useSoundForNotifications) {
709+
this.useSoundForNotificationsProperty.set(useSoundForNotifications);
710+
}
711+
700712
public void setTakeOfferSelectedPaymentAccountId(String value) {
701713
prefPayload.setTakeOfferSelectedPaymentAccountId(value);
702714
requestPersistence();
@@ -946,6 +958,8 @@ private interface ExcludesDelegateMethods {
946958

947959
void setUseStandbyMode(boolean useStandbyMode);
948960

961+
void setUseSoundForNotifications(boolean useSoundForNotifications);
962+
949963
void setTakeOfferSelectedPaymentAccountId(String value);
950964

951965
void setIgnoreDustThreshold(int value);

core/src/main/java/haveno/core/user/PreferencesPayload.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
108108
private boolean useMarketNotifications = true;
109109
private boolean usePriceNotifications = true;
110110
private boolean useStandbyMode = false;
111+
private boolean useSoundForNotifications = true;
111112
@Nullable
112113
private String rpcUser;
113114
@Nullable
@@ -185,6 +186,7 @@ public Message toProtoMessage() {
185186
.setUseMarketNotifications(useMarketNotifications)
186187
.setUsePriceNotifications(usePriceNotifications)
187188
.setUseStandbyMode(useStandbyMode)
189+
.setUseSoundForNotifications(useSoundForNotifications)
188190
.setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent)
189191
.setIgnoreDustThreshold(ignoreDustThreshold)
190192
.setClearDataAfterDays(clearDataAfterDays)
@@ -280,6 +282,7 @@ public static PreferencesPayload fromProto(protobuf.PreferencesPayload proto, Co
280282
proto.getUseMarketNotifications(),
281283
proto.getUsePriceNotifications(),
282284
proto.getUseStandbyMode(),
285+
proto.getUseSoundForNotifications(),
283286
proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(),
284287
proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(),
285288
proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(),

core/src/main/java/haveno/core/xmr/Balances.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public void onBalanceChanged(BigInteger balance) {
111111

112112
public XmrBalanceInfo getBalances() {
113113
synchronized (this) {
114+
if (availableBalance == null) return null;
114115
return new XmrBalanceInfo(availableBalance.longValue() + pendingBalance.longValue(),
115116
availableBalance.longValue(),
116117
pendingBalance.longValue(),
@@ -127,6 +128,9 @@ private void doUpdateBalances() {
127128
synchronized (this) {
128129
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
129130

131+
// get non-trade balance before
132+
BigInteger balanceSumBefore = getNonTradeBalanceSum();
133+
130134
// get wallet balances
131135
BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance();
132136
availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance();
@@ -160,8 +164,25 @@ private void doUpdateBalances() {
160164
reservedBalance = reservedOfferBalance.add(reservedTradeBalance);
161165

162166
// notify balance update
163-
UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1));
167+
UserThread.execute(() -> {
168+
169+
// check if funds received
170+
boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0;
171+
if (fundsReceived) {
172+
HavenoUtils.playCashRegisterSound();
173+
}
174+
175+
// increase counter to notify listeners
176+
updateCounter.set(updateCounter.get() + 1);
177+
});
164178
}
165179
}
166180
}
181+
182+
private BigInteger getNonTradeBalanceSum() {
183+
synchronized (this) {
184+
if (availableBalance == null) return null;
185+
return availableBalance.add(pendingBalance).add(reservedOfferBalance);
186+
}
187+
}
167188
}
797 KB
Binary file not shown.

0 commit comments

Comments
 (0)