@Override
  public void announce(AnnounceRequestMessage.RequestEvent event, boolean inhibitEvents)
      throws AnnounceException {
    logger.info(
        "Announcing{} to tracker with {}U/{}D/{}L bytes...",
        new Object[] {
          this.formatAnnounceEvent(event),
          this.torrent.getUploaded(),
          this.torrent.getDownloaded(),
          this.torrent.getLeft()
        });

    State state = State.CONNECT_REQUEST;
    int maxAttempts =
        AnnounceRequestMessage.RequestEvent.STOPPED.equals(event)
            ? UDP_MAX_TRIES_ON_STOPPED
            : UDP_MAX_TRIES;
    int attempts = -1;

    try {
      this.socket = new DatagramSocket();
      this.socket.connect(this.address);

      while (++attempts <= maxAttempts) {
        // Transaction ID is randomized for each exchange.
        this.transactionId = this.random.nextInt();

        // Immediately decide if we can send the announce request
        // directly or not. For this, we need a valid, non-expired
        // connection ID.
        if (this.connectionExpiration != null) {
          if (new Date().before(this.connectionExpiration)) {
            state = State.ANNOUNCE_REQUEST;
          } else {
            logger.debug("Announce connection ID expired, " + "reconnecting with tracker...");
          }
        }

        switch (state) {
          case CONNECT_REQUEST:
            this.send(UDPConnectRequestMessage.craft(this.transactionId).getData());

            try {
              this.handleTrackerConnectResponse(
                  UDPTrackerMessage.UDPTrackerResponseMessage.parse(this.recv(attempts)));
              attempts = -1;
            } catch (SocketTimeoutException ste) {
              // Silently ignore the timeout and retry with a
              // longer timeout, unless announce stop was
              // requested in which case we need to exit right
              // away.
              if (stop) {
                return;
              }
            }
            break;

          case ANNOUNCE_REQUEST:
            this.send(this.buildAnnounceRequest(event).getData());

            try {
              this.handleTrackerAnnounceResponse(
                  UDPTrackerMessage.UDPTrackerResponseMessage.parse(this.recv(attempts)),
                  inhibitEvents);
              // If we got here, we succesfully completed this
              // announce exchange and can simply return to exit the
              // loop.
              return;
            } catch (SocketTimeoutException ste) {
              // Silently ignore the timeout and retry with a
              // longer timeout, unless announce stop was
              // requested in which case we need to exit right
              // away.
              if (stop) {
                return;
              }
            }
            break;
          default:
            throw new IllegalStateException("Invalid announce state!");
        }
      }

      // When the maximum number of attempts was reached, the announce
      // really timed-out. We'll try again in the next announce loop.
      throw new AnnounceException(
          "Timeout while announcing" + this.formatAnnounceEvent(event) + " to tracker!");
    } catch (IOException ioe) {
      throw new AnnounceException(
          "Error while announcing"
              + this.formatAnnounceEvent(event)
              + " to tracker: "
              + ioe.getMessage(),
          ioe);
    } catch (MessageValidationException mve) {
      throw new AnnounceException(
          "Tracker message violates expected " + "protocol (" + mve.getMessage() + ")", mve);
    }
  }
  /**
   * Build, send and process a tracker announce request.
   *
   * <p>This function first builds an announce request for the specified event with all the required
   * parameters. Then, the request is made to the tracker and the response analyzed.
   *
   * <p>All registered {@link AnnounceResponseListener} objects are then fired with the decoded
   * payload.
   *
   * @param event The announce event type (can be AnnounceEvent.NONE for periodic updates).
   * @param inhibitEvents Prevent event listeners from being notified.
   */
  @Override
  public void announce(AnnounceRequestMessage.RequestEvent event, boolean inhibitEvents)
      throws AnnounceException {
    logger.info(
        "Announcing{} to tracker with {}U/{}D/{}L bytes...",
        new Object[] {
          this.formatAnnounceEvent(event),
          this.torrent.getUploaded(),
          this.torrent.getDownloaded(),
          this.torrent.getLeft()
        });

    URL target = null;
    try {
      HTTPAnnounceRequestMessage request = this.buildAnnounceRequest(event);
      target = request.buildAnnounceURL(this.tracker.toURL());
    } catch (MalformedURLException mue) {
      throw new AnnounceException("Invalid announce URL (" + mue.getMessage() + ")", mue);
    } catch (MessageValidationException mve) {
      throw new AnnounceException(
          "Announce request creation violated " + "expected protocol (" + mve.getMessage() + ")",
          mve);
    } catch (IOException ioe) {
      throw new AnnounceException(
          "Error building announce request (" + ioe.getMessage() + ")", ioe);
    }

    HttpURLConnection conn = null;
    InputStream in = null;
    try {
      conn = (HttpURLConnection) target.openConnection();
      in = conn.getInputStream();
    } catch (IOException ioe) {
      if (conn != null) {
        in = conn.getErrorStream();
      }
    }

    // At this point if the input stream is null it means we have neither a
    // response body nor an error stream from the server. No point in going
    // any further.
    if (in == null) {
      throw new AnnounceException("No response or unreachable tracker!");
    }

    try {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      baos.write(in);

      // Parse and handle the response
      HTTPTrackerMessage message = HTTPTrackerMessage.parse(ByteBuffer.wrap(baos.toByteArray()));
      baos.close(); // close stream
      this.handleTrackerAnnounceResponse(message, inhibitEvents);
    } catch (IOException ioe) {
      throw new AnnounceException("Error reading tracker response!", ioe);
    } catch (MessageValidationException mve) {
      throw new AnnounceException(
          "Tracker message violates expected " + "protocol (" + mve.getMessage() + ")", mve);
    } finally {
      // Make sure we close everything down at the end to avoid resource
      // leaks.
      try {
        in.close();
      } catch (IOException ioe) {
        logger.warn("Problem ensuring error stream closed!", ioe);
      }

      // This means trying to close the error stream as well.
      InputStream err = conn.getErrorStream();
      if (err != null) {
        try {
          err.close();
        } catch (IOException ioe) {
          logger.warn("Problem ensuring error stream closed!", ioe);
        }
      }
    }
  }