mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-27 08:55:22 -04:00
play sounds on notifications #1284
This commit is contained in:
parent
b2a6708ac1
commit
b940021d99
29 changed files with 173 additions and 16 deletions
|
@ -3,7 +3,11 @@ package haveno.core.api;
|
|||
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;
|
||||
|
@ -46,7 +50,15 @@ public class CoreNotificationService {
|
|||
.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())
|
||||
|
@ -57,6 +69,7 @@ public class CoreNotificationService {
|
|||
}
|
||||
|
||||
public void sendChatNotification(ChatMessage chatMessage) {
|
||||
HavenoUtils.playChimeSound();
|
||||
sendNotification(NotificationMessage.newBuilder()
|
||||
.setType(NotificationType.CHAT_MESSAGE)
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
|
|
|
@ -98,7 +98,7 @@ public class XmrBalanceInfo implements Payload {
|
|||
public String toString() {
|
||||
return "XmrBalanceInfo{" +
|
||||
"balance=" + balance +
|
||||
"unlockedBalance=" + availableBalance +
|
||||
", unlockedBalance=" + availableBalance +
|
||||
", lockedBalance=" + pendingBalance +
|
||||
", reservedOfferBalance=" + reservedOfferBalance +
|
||||
", reservedTradeBalance=" + reservedTradeBalance +
|
||||
|
|
|
@ -53,6 +53,7 @@ import haveno.core.alert.Alert;
|
|||
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;
|
||||
|
@ -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;
|
||||
|
@ -228,6 +232,7 @@ public class HavenoSetup {
|
|||
User user,
|
||||
AlertManager alertManager,
|
||||
Config config,
|
||||
CoreContext coreContext,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
TorSetup torSetup,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
|
@ -253,6 +258,7 @@ public class HavenoSetup {
|
|||
this.user = user;
|
||||
this.alertManager = alertManager;
|
||||
this.config = config;
|
||||
this.coreContext = coreContext;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.torSetup = torSetup;
|
||||
this.formatter = formatter;
|
||||
|
@ -263,6 +269,7 @@ public class HavenoSetup {
|
|||
this.arbitrationManager = arbitrationManager;
|
||||
|
||||
HavenoUtils.havenoSetup = this;
|
||||
HavenoUtils.preferences = preferences;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -27,7 +27,9 @@ import haveno.common.crypto.Hash;
|
|||
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;
|
||||
|
@ -36,9 +38,12 @@ import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
|||
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;
|
||||
|
@ -53,6 +58,13 @@ import java.util.Date;
|
|||
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;
|
||||
|
@ -110,11 +122,18 @@ public class HavenoUtils {
|
|||
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;
|
||||
|
@ -533,4 +552,60 @@ public class HavenoUtils {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -649,6 +649,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
|||
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(() -> {
|
||||
|
@ -2833,6 +2834,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
|||
// 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());
|
||||
}
|
||||
|
@ -2841,6 +2843,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
|||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -68,7 +68,6 @@ import haveno.core.support.dispute.mediation.mediator.MediatorManager;
|
|||
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;
|
||||
|
@ -134,7 +133,6 @@ import lombok.Setter;
|
|||
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;
|
||||
|
||||
|
@ -258,7 +256,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -599,14 +599,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
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);
|
||||
|
|
|
@ -132,6 +132,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
|||
private final String xmrNodesFromOptions;
|
||||
@Getter
|
||||
private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode());
|
||||
@Getter
|
||||
private final BooleanProperty useSoundForNotificationsProperty = new SimpleBooleanProperty(prefPayload.isUseSoundForNotifications());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
|
@ -162,6 +164,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
|||
requestPersistence();
|
||||
});
|
||||
|
||||
useSoundForNotificationsProperty.addListener((ov) -> {
|
||||
prefPayload.setUseSoundForNotifications(useSoundForNotificationsProperty.get());
|
||||
requestPersistence();
|
||||
});
|
||||
|
||||
traditionalCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> {
|
||||
prefPayload.getTraditionalCurrencies().clear();
|
||||
prefPayload.getTraditionalCurrencies().addAll(traditionalCurrenciesAsObservable);
|
||||
|
@ -259,6 +266,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
|||
// set all properties
|
||||
useAnimationsProperty.set(prefPayload.isUseAnimations());
|
||||
useStandbyModeProperty.set(prefPayload.isUseStandbyMode());
|
||||
useSoundForNotificationsProperty.set(prefPayload.isUseSoundForNotifications());
|
||||
cssThemeProperty.set(prefPayload.getCssTheme());
|
||||
|
||||
|
||||
|
@ -697,6 +705,10 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
|||
this.useStandbyModeProperty.set(useStandbyMode);
|
||||
}
|
||||
|
||||
public void setUseSoundForNotifications(boolean useSoundForNotifications) {
|
||||
this.useSoundForNotificationsProperty.set(useSoundForNotifications);
|
||||
}
|
||||
|
||||
public void setTakeOfferSelectedPaymentAccountId(String value) {
|
||||
prefPayload.setTakeOfferSelectedPaymentAccountId(value);
|
||||
requestPersistence();
|
||||
|
@ -946,6 +958,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
|||
|
||||
void setUseStandbyMode(boolean useStandbyMode);
|
||||
|
||||
void setUseSoundForNotifications(boolean useSoundForNotifications);
|
||||
|
||||
void setTakeOfferSelectedPaymentAccountId(String value);
|
||||
|
||||
void setIgnoreDustThreshold(int value);
|
||||
|
|
|
@ -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
|
||||
|
@ -185,6 +186,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
|||
.setUseMarketNotifications(useMarketNotifications)
|
||||
.setUsePriceNotifications(usePriceNotifications)
|
||||
.setUseStandbyMode(useStandbyMode)
|
||||
.setUseSoundForNotifications(useSoundForNotifications)
|
||||
.setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent)
|
||||
.setIgnoreDustThreshold(ignoreDustThreshold)
|
||||
.setClearDataAfterDays(clearDataAfterDays)
|
||||
|
@ -280,6 +282,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
|||
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(),
|
||||
|
|
|
@ -111,6 +111,7 @@ public class Balances {
|
|||
|
||||
public XmrBalanceInfo getBalances() {
|
||||
synchronized (this) {
|
||||
if (availableBalance == null) return null;
|
||||
return new XmrBalanceInfo(availableBalance.longValue() + pendingBalance.longValue(),
|
||||
availableBalance.longValue(),
|
||||
pendingBalance.longValue(),
|
||||
|
@ -127,6 +128,9 @@ public class Balances {
|
|||
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();
|
||||
|
@ -160,8 +164,25 @@ public class Balances {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue