Exemple #1
0
 public void actionPerformed(ActionEvent e) {
   try {
     undo.redo();
   } catch (CannotRedoException ex) {
     Logger.getLogger(RedoAction.class.getName()).log(Level.SEVERE, "Unable to redo", ex);
   }
   update();
   undoAction.update();
 }
Exemple #2
0
    @Override
    @SuppressWarnings("SleepWhileHoldingLock")
    public void run() {
      try {
        // initialize the statusbar
        status.removeAll();
        JProgressBar progress = new JProgressBar();
        progress.setMinimum(0);
        progress.setMaximum(doc.getLength());
        status.add(progress);
        status.revalidate();

        // start writing
        Writer out = new FileWriter(f);
        Segment text = new Segment();
        text.setPartialReturn(true);
        int charsLeft = doc.getLength();
        int offset = 0;
        while (charsLeft > 0) {
          doc.getText(offset, Math.min(4096, charsLeft), text);
          out.write(text.array, text.offset, text.count);
          charsLeft -= text.count;
          offset += text.count;
          progress.setValue(offset);
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            Logger.getLogger(FileSaver.class.getName()).log(Level.SEVERE, null, e);
          }
        }
        out.flush();
        out.close();
      } catch (IOException e) {
        final String msg = e.getMessage();
        SwingUtilities.invokeLater(
            new Runnable() {

              public void run() {
                JOptionPane.showMessageDialog(
                    getFrame(),
                    "Could not save file: " + msg,
                    "Error saving file",
                    JOptionPane.ERROR_MESSAGE);
              }
            });
      } catch (BadLocationException e) {
        System.err.println(e.getMessage());
      }
      // we are done... get rid of progressbar
      status.removeAll();
      status.revalidate();
    }
/**
 * Dialog box to get configuration options for client application. This class provides a standard
 * dialog box which allows the user to select the location of the database (which may be a physical
 * file in local mode, or the address (and, optionally, the port) of the server. The user can, of
 * course, cancel, in which case the application should not start (this is at the applications
 * discretion though - the business logic could be changed later in the calling class to decide to
 * start the application anyway if there configuration info can be loaded from file).<br>
 */
public class DatabaseLocationDialog extends WindowAdapter implements ActionListener, Observer {
  /*
   * The strings for the title and buttons in the dialog box. While these
   * could be hard coded in the application, having them here makes it
   * easier to use internationalization options such as a ResourceBundle.
   */
  private static final String TITLE = "Please enter database location";
  private static final String CONNECT = "Connect";
  private static final String EXIT = "Exit";

  /*
   * Some values for possible port ranges so we can determine what sort of
   * port the user has specified.
   */
  private static final int LOWEST_PORT = 0;
  private static final int HIGHEST_PORT = 65535;
  private static final int SYSTEM_PORT_BOUNDARY = 1024;
  private static final int IANA_PORT_BOUNDARY = 49151;

  /*
   * The bits and pieces that comprise our dialog box. They are all global so
   * we can disable them or enable them as the user enters valid information.
   */
  private JOptionPane options = null;
  private JDialog dialog = null;
  private JButton connectButton = new JButton(CONNECT);
  private JButton exitButton = new JButton(EXIT);

  /*
   * The common panel that is used by both the client and the server for
   * specifying where the database is.
   */
  private ConfigOptions configOptions = null;

  /*
   * Flags to show whether enough information has been provided for us to
   * start the application.
   */
  private boolean validDb = false;
  private boolean validPort = false;
  private boolean validCnx = false;

  /*
   * Details specified in the configOptions pane detailing where the database
   * is.
   */
  private String location = null;
  private String port = null;
  private ConnectionType networkType = null;

  /*
   * The Logger instance. All log messages from this class are routed through
   * this member. The Logger namespace is <code>sampleproject.gui</code>.
   */
  private Logger log = Logger.getLogger("sampleproject.gui");

  /**
   * Creates a dialog where the user can specify the location of the database,including the type of
   * network connection (if this is a networked client)and IP address and port number; or search and
   * select the database on a local drive if this is a standalone client.
   *
   * @param parent Defines the Component that is to be the parent of this dialog box. For
   *     information on how this is used, see <code>JOptionPane</code>
   * @param connectionMode Specifies the type of connection (standalone or networked)
   * @see JOptionPane
   */
  public DatabaseLocationDialog(Frame parent, ApplicationMode connectionMode) {
    configOptions = (new ConfigOptions(connectionMode));
    configOptions.getObservable().addObserver(this);

    // load saved configuration
    SavedConfiguration config = SavedConfiguration.getSavedConfiguration();

    // the port and connection type are irrelevant in standalone mode
    if (connectionMode == ApplicationMode.STANDALONE_CLIENT) {
      validPort = true;
      validCnx = true;
      networkType = ConnectionType.DIRECT;
      location = config.getParameter(SavedConfiguration.DATABASE_LOCATION);
    } else {
      // there may not be a network connectivity type defined and, if
      // not, we do not set a default - force the user to make a choice
      // the at least for the first time they run this.
      String tmp = config.getParameter(SavedConfiguration.NETWORK_TYPE);
      if (tmp != null) {
        try {
          networkType = ConnectionType.valueOf(tmp);
          configOptions.setNetworkConnection(networkType);
          validCnx = true;
        } catch (IllegalArgumentException e) {
          log.warning("Unknown connection type: " + networkType);
        }
      }

      // there is always at least a default port number, so we don't have
      // to validate this.
      port = config.getParameter(SavedConfiguration.SERVER_PORT);
      configOptions.setPortNumberText(port);
      validPort = true;

      location = config.getParameter(SavedConfiguration.SERVER_ADDRESS);
    }

    // there may not be a default database location, so we had better
    // validate before using the returned value.
    if (location != null) {
      configOptions.setLocationFieldText(location);
      validDb = true;
    }

    options =
        new JOptionPane(configOptions, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION);

    connectButton.setActionCommand(CONNECT);
    connectButton.addActionListener(this);

    boolean allValid = validDb && validPort && validCnx;
    connectButton.setEnabled(allValid);

    exitButton.setActionCommand(EXIT);
    exitButton.addActionListener(this);

    options.setOptions(new Object[] {connectButton, exitButton});

    dialog = options.createDialog(parent, TITLE);
    dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
    dialog.addWindowListener(this);
    dialog.setVisible(true);
  }

  // Note: we can get away with not specifying the parameters in these
  // callback methods, as they are specified in the interfaces we are
  // implementing.

  /**
   * Callback event handler to process situations where the user has closed the window rather than
   * clicking one of the buttons.
   */
  public void windowClosing(WindowEvent we) {
    processCommand(EXIT);
  }

  /** Callback event handler to process clicks on any of the buttons. */
  public void actionPerformed(ActionEvent ae) {
    processCommand(ae.getActionCommand());
  }

  /**
   * Common event handling code - can handle desirable actions (such as buttons being clicked) and
   * undesirable actions (the window being closed) all in a common location.
   *
   * @param command a String representing the action that occurred.
   */
  private void processCommand(String command) {
    dialog.setVisible(false);
    if (CONNECT.equals(command)) {
      options.setValue(JOptionPane.OK_OPTION);
    } else {
      options.setValue(JOptionPane.CANCEL_OPTION);
    }
  }

  /**
   * Callback method to process modifications in the common ConfigOptions panel. ConfigOptions was
   * developed to be common to many applications (even though we only have three modes), so it does
   * not have any knowledge of how we are using it within this dialog box. So ConfigOptions just
   * sends updates to registered Observers whenever anything changes. We can receive those
   * notifications here, and decide whether we have enough information to enable the "Connect"
   * button of the dialog box.
   */
  public void update(Observable o, Object arg) {
    // we are going to ignore the Observable object, since we are only
    // observing one object. All we are interested in is the argument.

    if (!(arg instanceof OptionUpdate)) {
      log.log(
          Level.WARNING,
          "DatabaseLocationDialog received update type: " + arg,
          new IllegalArgumentException());
      return;
    }

    OptionUpdate optionUpdate = (OptionUpdate) arg;

    // load saved configuration
    SavedConfiguration config = SavedConfiguration.getSavedConfiguration();

    switch (optionUpdate.getUpdateType()) {
      case DB_LOCATION_CHANGED:
        location = (String) optionUpdate.getPayload();
        if (configOptions.getApplicationMode() == ApplicationMode.STANDALONE_CLIENT) {
          File f = new File(location);
          if (f.exists() && f.canRead() && f.canWrite()) {
            validDb = true;
            log.info("File chosen " + location);
            config.setParameter(SavedConfiguration.DATABASE_LOCATION, location);
          } else {
            log.warning("Invalid file " + location);
          }
        } else {
          try {
            if (location.matches("\\d+\\.\\d+\\.\\d+\\.\\d+")) {
              // location given matches 4 '.' separated numbers
              // regex could be improved by limiting each quad to
              // no more than 3 digits.
              String[] quads = location.split("\\.");
              byte[] address = new byte[quads.length];
              for (int i = 0; i < quads.length; i++) {
                address[i] = new Integer(quads[i]).byteValue();
              }
              InetAddress.getByAddress(address);
            } else {
              InetAddress.getAllByName(location);
            }
            log.info("Server specified " + location);
            validDb = true;
            config.setParameter(SavedConfiguration.SERVER_ADDRESS, location);
          } catch (UnknownHostException uhe) {
            log.warning("Unknown host: " + location);
            validDb = false;
          }
        }
        break;
      case PORT_CHANGED:
        port = (String) optionUpdate.getPayload();
        int p = Integer.parseInt(port);

        if (p >= LOWEST_PORT && p < HIGHEST_PORT) {
          if (p < SYSTEM_PORT_BOUNDARY) {
            log.info("User chose System port " + port);
          } else if (p < IANA_PORT_BOUNDARY) {
            log.info("User chose IANA port " + port);
          } else {
            log.info("User chose dynamic port " + port);
          }
          validPort = true;
          config.setParameter(SavedConfiguration.SERVER_PORT, port);
        } else {
          validPort = false;
        }
        break;
      case NETWORK_CHOICE_MADE:
        networkType = (ConnectionType) optionUpdate.getPayload();
        switch (networkType) {
          case SOCKET:
            log.info("Server connection via Sockets");
            break;
          case RMI:
            log.info("Server connection via RMI");
            break;
          default:
            log.info("Unknown connection type: " + networkType);
            break;
        }
        config.setParameter(SavedConfiguration.NETWORK_TYPE, "" + networkType);
        validCnx = true;
        break;
      default:
        log.warning("Unknown update: " + optionUpdate);
        break;
    }

    boolean allValid = validDb && validPort && validCnx;
    connectButton.setEnabled(allValid);
  }

  /**
   * Returns the location of the database, which may be either the path to the local database, or
   * the address of the network server hosting the database.
   *
   * @return the location of the database.
   */
  public String getLocation() {
    return location;
  }

  /**
   * Returns the port number the network server should be listening on for client connections.
   *
   * @return the port number for connecting to the network server.
   */
  public String getPort() {
    return port;
  }

  /**
   * Returns the type of network connection (Sockets, RMI ...) that should be used to connect to the
   * server.
   *
   * @return the network protocol used to connect to the server.
   */
  public ConnectionType getNetworkType() {
    return networkType;
  }

  /**
   * Let the caller of this dialog know whether the user connected or cancelled.
   *
   * @return true if the user cancelled or closed the window.
   */
  public boolean userCanceled() {
    if (options.getValue() instanceof Integer) {
      int status = ((Integer) options.getValue()).intValue();
      return status != JOptionPane.OK_OPTION;
    } else {
      return false;
    }
  }
}
/**
 * The <tt>OneToOneCallPeerPanel</tt> is the panel containing data for a call peer in a given call.
 * It contains information like call peer name, photo, call duration, etc.
 *
 * @author Yana Stamcheva
 * @author Lyubomir Marinov
 * @author Sebastien Vincent
 * @author Adam Netocny
 */
public class OneToOneCallPeerPanel extends TransparentPanel
    implements SwingCallPeerRenderer, PropertyChangeListener, Skinnable {
  /**
   * The <tt>Logger</tt> used by the <tt>OneToOneCallPeerPanel</tt> class and its instances for
   * logging output.
   */
  private static final Logger logger = Logger.getLogger(OneToOneCallPeerPanel.class);

  /** Serial version UID. */
  private static final long serialVersionUID = 0L;

  /** The <tt>CallPeer</tt>, which is rendered in this panel. */
  private final CallPeer callPeer;

  /** The <tt>Call</tt>, which is rendered in this panel. */
  private final Call call;

  /**
   * The <tt>CallPeerAdapter</tt> which implements common <tt>CallPeer</tt>-related listeners on
   * behalf of this instance.
   */
  private final CallPeerAdapter callPeerAdapter;

  /** The renderer of the call. */
  private final SwingCallRenderer callRenderer;

  /** The component showing the status of the underlying call peer. */
  private final JLabel callStatusLabel = new JLabel();

  /** The center component. */
  private final VideoContainer center;

  /**
   * The AWT <tt>Component</tt> which implements a button which allows closing/hiding the visual
   * <tt>Component</tt> which depicts the video streaming from the local peer/user to the remote
   * peer(s).
   */
  private Component closeLocalVisualComponentButton;

  /**
   * A listener to desktop sharing granted/revoked events and to mouse and keyboard interaction with
   * the remote video displaying the remote desktop.
   */
  private final DesktopSharingMouseAndKeyboardListener desktopSharingMouseAndKeyboardListener;

  /**
   * The indicator which determines whether {@link #dispose()} has already been invoked on this
   * instance. If <tt>true</tt>, this instance is considered non-functional and is to be left to the
   * garbage collector.
   */
  private boolean disposed = false;

  /** The DTMF label. */
  private final JLabel dtmfLabel = new JLabel();

  /** The component responsible for displaying an error message. */
  private JTextComponent errorMessageComponent;

  /** The label showing whether the call is on or off hold. */
  private final JLabel holdStatusLabel = new JLabel();

  /** Sound local level label. */
  private InputVolumeControlButton localLevel;

  /**
   * The <tt>Component</tt> which {@link #updateViewFromModelInEventDispatchThread()} last added to
   * {@link #center} as the visual <tt>Component</tt> displaying the video streaming from the local
   * peer/user to the remote peer(s).
   *
   * <p><b>Warning</b>: It is not to be used for any other purposes because it may not represent the
   * current state of the model of this view.
   */
  private Component localVideo;

  /** The label showing whether the voice has been set to mute. */
  private final JLabel muteStatusLabel = new JLabel();

  /** The <tt>Icon</tt> which represents the avatar of the associated call peer. */
  private ImageIcon peerImage;

  /** The name of the peer. */
  private String peerName;

  /** The label containing the user photo. */
  private final JLabel photoLabel;

  /** Sound remote level label. */
  private Component remoteLevel;

  /**
   * The <tt>Component</tt> which {@link #updateViewFromModelInEventDispatchThread()} last added to
   * {@link #center} as the visual <tt>Component</tt> displaying the video streaming from the remote
   * peer(s) to the local peer/user.
   *
   * <p><b>Warning</b>: It is not to be used for any other purposes because it may not represent the
   * current state of the model of this view.
   */
  private Component remoteVideo;

  /** Current id for security image. */
  private ImageID securityImageID = ImageLoader.SECURE_BUTTON_OFF;

  /** The panel containing security related components. */
  private SecurityPanel<?> securityPanel;

  /** The security status of the peer */
  private final SecurityStatusLabel securityStatusLabel = new SecurityStatusLabel();

  /** The status bar component. */
  private final Component statusBar;

  /** The facility which aids this instance in the dealing with the video-related information. */
  private final UIVideoHandler2 uiVideoHandler;

  /**
   * The <tt>Observer</tt> which listens to changes in the video-related information detected and
   * reported by {@link #uiVideoHandler}.
   */
  private final Observer uiVideoHandlerObserver =
      new Observer() {
        public void update(Observable o, Object arg) {
          updateViewFromModel();
        }
      };

  /**
   * The <tt>Runnable</tt> which is scheduled by {@link #updateViewFromModel()} for execution in the
   * AWT event dispatching thread in order to invoke {@link
   * #updateViewFromModelInEventDispatchThread()}.
   */
  private final Runnable updateViewFromModelInEventDispatchThread =
      new Runnable() {
        public void run() {
          /*
           * We receive events/notifications from various threads and we
           * respond to them in the AWT event dispatching thread. It is
           * possible to first schedule an event to be brought to the AWT
           * event dispatching thread, then to have #dispose() invoked on
           * this instance and, finally, to receive the scheduled event in
           * the AWT event dispatching thread. In such a case, this
           * disposed instance should not respond to the event.
           */
          if (!disposed) updateViewFromModelInEventDispatchThread();
        }
      };

  /**
   * Creates a <tt>CallPeerPanel</tt> for the given call peer.
   *
   * @param callRenderer the renderer of the call
   * @param callPeer the <tt>CallPeer</tt> represented in this panel
   * @param uiVideoHandler the facility which is to aid the new instance in the dealing with the
   *     video-related information
   */
  public OneToOneCallPeerPanel(
      SwingCallRenderer callRenderer, CallPeer callPeer, UIVideoHandler2 uiVideoHandler) {
    this.callRenderer = callRenderer;
    this.callPeer = callPeer;
    // we need to obtain call as soon as possible
    // cause if it fails too quickly we may fail to show it
    this.call = callPeer.getCall();
    this.uiVideoHandler = uiVideoHandler;

    peerName = CallManager.getPeerDisplayName(callPeer);
    securityPanel = SecurityPanel.create(this, callPeer, null);

    photoLabel = new JLabel(getPhotoLabelIcon());
    center = createCenter();
    statusBar = createStatusBar();

    setPeerImage(CallManager.getPeerImage(callPeer));

    /* Lay out the main Components of the UI. */
    setLayout(new GridBagLayout());

    GridBagConstraints cnstrnts = new GridBagConstraints();

    if (center != null) {
      cnstrnts.fill = GridBagConstraints.BOTH;
      cnstrnts.gridx = 0;
      cnstrnts.gridy = 1;
      cnstrnts.weightx = 1;
      cnstrnts.weighty = 1;
      add(center, cnstrnts);
    }
    if (statusBar != null) {
      cnstrnts.fill = GridBagConstraints.NONE;
      cnstrnts.gridx = 0;
      cnstrnts.gridy = 3;
      cnstrnts.weightx = 0;
      cnstrnts.weighty = 0;
      cnstrnts.insets = new Insets(5, 0, 0, 0);
      add(statusBar, cnstrnts);
    }

    createSoundLevelIndicators();
    initSecuritySettings();

    /*
     * Add the listeners which will be notified about changes in the model
     * and which will update this view.
     */
    callPeerAdapter = new CallPeerAdapter(callPeer, this);
    uiVideoHandler.addObserver(uiVideoHandlerObserver);

    /*
     * This view adapts to whether it is displayed in full-screen or
     * windowed mode.
     */
    if (callRenderer instanceof Component) {
      ((Component) callRenderer).addPropertyChangeListener(CallContainer.PROP_FULL_SCREEN, this);
    }

    OperationSetDesktopSharingClient desktopSharingClient =
        callPeer.getProtocolProvider().getOperationSet(OperationSetDesktopSharingClient.class);
    if (desktopSharingClient != null) {
      desktopSharingMouseAndKeyboardListener =
          new DesktopSharingMouseAndKeyboardListener(callPeer, desktopSharingClient);
    } else desktopSharingMouseAndKeyboardListener = null;

    updateViewFromModel();
  }

  /**
   * Creates the <tt>Component</tt> hierarchy of the central area of this <tt>CallPeerPanel</tt>
   * which displays the photo of the <tt>CallPeer</tt> or the video if any.
   */
  private VideoContainer createCenter() {
    photoLabel.setPreferredSize(new Dimension(90, 90));

    return createVideoContainer(photoLabel);
  }

  /** Creates sound level related components. */
  private void createSoundLevelIndicators() {
    TransparentPanel localLevelPanel = new TransparentPanel(new BorderLayout(5, 0));
    TransparentPanel remoteLevelPanel = new TransparentPanel(new BorderLayout(5, 0));

    localLevel =
        new InputVolumeControlButton(
            call, ImageLoader.MICROPHONE, ImageLoader.MUTE_BUTTON, false, false);
    remoteLevel =
        new OutputVolumeControlButton(call.getConference(), ImageLoader.HEADPHONE, false, false)
            .getComponent();

    final SoundLevelIndicator localLevelIndicator =
        new SoundLevelIndicator(SoundLevelChangeEvent.MIN_LEVEL, SoundLevelChangeEvent.MAX_LEVEL);
    final SoundLevelIndicator remoteLevelIndicator =
        new SoundLevelIndicator(SoundLevelChangeEvent.MIN_LEVEL, SoundLevelChangeEvent.MAX_LEVEL);

    localLevelPanel.add(localLevel, BorderLayout.WEST);
    localLevelPanel.add(localLevelIndicator, BorderLayout.CENTER);
    remoteLevelPanel.add(remoteLevel, BorderLayout.WEST);
    remoteLevelPanel.add(remoteLevelIndicator, BorderLayout.CENTER);

    GridBagConstraints constraints = new GridBagConstraints();
    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = 0;
    constraints.gridy = 5;
    constraints.weightx = 0;
    constraints.weighty = 0;
    constraints.insets = new Insets(10, 0, 0, 0);

    add(localLevelPanel, constraints);

    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = 0;
    constraints.gridy = 6;
    constraints.weightx = 0;
    constraints.weighty = 0;
    constraints.insets = new Insets(5, 0, 10, 0);

    add(remoteLevelPanel, constraints);

    if (!GuiActivator.getConfigurationService()
        .getBoolean(
            "net.java.sip.communicator.impl.gui.main.call." + "DISABLE_SOUND_LEVEL_INDICATORS",
            false)) {
      callPeer.addStreamSoundLevelListener(
          new SoundLevelListener() {
            public void soundLevelChanged(Object source, int level) {
              remoteLevelIndicator.updateSoundLevel(level);
            }
          });
      /*
       * By the time the UI gets to be initialized, the callPeer may have
       * been removed from its Call. As far as the UI is concerned, the
       * callPeer will never have a Call again and there will be no audio
       * levels to display anyway so there is no point in throwing a
       * NullPointerException here.
       */
      if (call != null) {
        call.addLocalUserSoundLevelListener(
            new SoundLevelListener() {
              public void soundLevelChanged(Object source, int level) {
                localLevelIndicator.updateSoundLevel(level);
              }
            });
      }
    }
  }

  /**
   * Creates the <tt>Component</tt> hierarchy of the area of status-related information such as
   * <tt>CallPeer</tt> display name, call duration, security status.
   *
   * @return the root of the <tt>Component</tt> hierarchy of the area of status-related information
   *     such as <tt>CallPeer</tt> display name, call duration, security status
   */
  private Component createStatusBar() {
    // stateLabel
    callStatusLabel.setForeground(Color.WHITE);
    dtmfLabel.setForeground(Color.WHITE);
    callStatusLabel.setText(callPeer.getState().getLocalizedStateString());

    PeerStatusPanel statusPanel = new PeerStatusPanel(new GridBagLayout());

    GridBagConstraints constraints = new GridBagConstraints();

    constraints.gridx = 0;
    constraints.gridy = 0;
    statusPanel.add(securityStatusLabel, constraints);
    initSecurityStatusLabel();

    constraints.gridx++;
    statusPanel.add(holdStatusLabel, constraints);

    constraints.gridx++;
    statusPanel.add(muteStatusLabel, constraints);

    constraints.gridx++;
    callStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 3, 2, 12));
    statusPanel.add(callStatusLabel, constraints);

    constraints.gridx++;
    constraints.weightx = 1f;
    statusPanel.add(dtmfLabel, constraints);

    return statusPanel;
  }

  /**
   * Creates a new AWT <tt>Container</tt> which can display a single <tt>Component</tt> at a time
   * (supposedly, one which represents video) and, in the absence of such a <tt>Component</tt>,
   * displays a predefined default <tt>Component</tt> (in accord with the previous supposition, one
   * which is the default when there is no video). The returned <tt>Container</tt> will track the
   * <tt>Components</tt>s added to and removed from it in order to make sure that
   * <tt>noVideoContainer</tt> is displayed as described.
   *
   * @param noVideoComponent the predefined default <tt>Component</tt> to be displayed in the
   *     returned <tt>Container</tt> when there is no other <tt>Component</tt> in it
   * @return a new <tt>Container</tt> which can display a single <tt>Component</tt> at a time and,
   *     in the absence of such a <tt>Component</tt>, displays <tt>noVideoComponent</tt>
   */
  private VideoContainer createVideoContainer(Component noVideoComponent) {
    Container oldParent = noVideoComponent.getParent();

    if (oldParent != null) oldParent.remove(noVideoComponent);

    return new VideoContainer(noVideoComponent, false);
  }

  /**
   * Releases the resources acquired by this instance which require explicit disposal (e.g. any
   * listeners added to the depicted <tt>CallPeer</tt>. Invoked by <tt>OneToOneCallPanel</tt> when
   * it determines that this <tt>OneToOneCallPeerPanel</tt> is no longer necessary.
   */
  public void dispose() {
    disposed = true;

    callPeerAdapter.dispose();
    uiVideoHandler.deleteObserver(uiVideoHandlerObserver);

    if (callRenderer instanceof Component) {
      ((Component) callRenderer).removePropertyChangeListener(CallContainer.PROP_FULL_SCREEN, this);
    }
  }

  /**
   * Returns the parent <tt>CallPanel</tt> containing this renderer.
   *
   * @return the parent <tt>CallPanel</tt> containing this renderer
   */
  public CallPanel getCallPanel() {
    return callRenderer.getCallContainer();
  }

  /**
   * Returns the parent call renderer.
   *
   * @return the parent call renderer
   */
  public CallRenderer getCallRenderer() {
    return callRenderer;
  }

  /**
   * Returns the component associated with this renderer.
   *
   * @return the component associated with this renderer
   */
  public Component getComponent() {
    return this;
  }

  /**
   * Returns the name of the peer, contained in this panel.
   *
   * @return the name of the peer, contained in this panel
   */
  public String getPeerName() {
    return peerName;
  }

  /**
   * Gets the <tt>Icon</tt> to be displayed in {@link #photoLabel}.
   *
   * @return the <tt>Icon</tt> to be displayed in {@link #photoLabel}
   */
  private ImageIcon getPhotoLabelIcon() {
    return (peerImage == null)
        ? new ImageIcon(ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO))
        : peerImage;
  }

  /** Initializes the security settings for this call peer. */
  private void initSecuritySettings() {
    CallPeerSecurityStatusEvent securityEvent = callPeer.getCurrentSecuritySettings();

    if (securityEvent instanceof CallPeerSecurityOnEvent)
      securityOn((CallPeerSecurityOnEvent) securityEvent);
  }

  /** Initializes the security status label, shown in the call status bar. */
  private void initSecurityStatusLabel() {
    securityStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));

    securityStatusLabel.addMouseListener(
        new MouseAdapter() {
          /** Invoked when a mouse button has been pressed on a component. */
          @Override
          public void mousePressed(MouseEvent e) {
            // Only show the security details if the security is on.
            SrtpControl ctrl = securityPanel.getSecurityControl();
            if (ctrl instanceof ZrtpControl && ctrl.getSecureCommunicationStatus()) {
              setSecurityPanelVisible(
                  !callRenderer
                      .getCallContainer()
                      .getCallWindow()
                      .getFrame()
                      .getGlassPane()
                      .isVisible());
            }
          }
        });
  }

  /**
   * Determines whether the visual <tt>Component</tt> depicting the video streaming from the local
   * peer/user to the remote peer(s) is currently visible.
   *
   * @return <tt>true</tt> if the visual <tt>Component</tt> depicting the video streaming from the
   *     local peer/user to the remote peer(s) is currently visible; otherwise, <tt>false</tt>
   */
  public boolean isLocalVideoVisible() {
    return uiVideoHandler.isLocalVideoVisible();
  }

  /** Reloads all icons. */
  public void loadSkin() {
    if (localLevel != null)
      localLevel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.MICROPHONE)));

    if (remoteLevel != null && remoteLevel instanceof Skinnable)
      ((Skinnable) remoteLevel).loadSkin();

    if (muteStatusLabel.getIcon() != null)
      muteStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON)));

    if (holdStatusLabel.getIcon() != null)
      holdStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON)));

    securityStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(securityImageID)));

    if (peerImage == null) {
      photoLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO)));
    }
  }

  /**
   * Prints the given DTMG character through this <tt>CallPeerRenderer</tt>.
   *
   * @param dtmfChar the DTMF char to print
   */
  public void printDTMFTone(char dtmfChar) {
    dtmfLabel.setText(dtmfLabel.getText() + dtmfChar);
    if (dtmfLabel.getBorder() == null)
      dtmfLabel.setBorder(BorderFactory.createEmptyBorder(2, 1, 2, 5));
  }

  /**
   * Notifies this instance about a change in the value of a property of a source which of interest
   * to this instance. For example, <tt>OneToOneCallPeerPanel</tt> updates its user
   * interface-related properties upon changes in the value of the {@link
   * CallContainer#PROP_FULL_SCREEN} property of its associated {@link #callRenderer}.
   *
   * @param ev a <tt>PropertyChangeEvent</tt> which identifies the source, the name of the property
   *     and the old and new values
   */
  public void propertyChange(PropertyChangeEvent ev) {
    if (CallContainer.PROP_FULL_SCREEN.equals(ev.getPropertyName())) updateViewFromModel();
  }

  /**
   * Re-dispatches glass pane mouse events only in case they occur on the security panel.
   *
   * @param glassPane the glass pane
   * @param e the mouse event in question
   */
  private void redispatchMouseEvent(Component glassPane, MouseEvent e) {
    Point glassPanePoint = e.getPoint();

    Point securityPanelPoint =
        SwingUtilities.convertPoint(glassPane, glassPanePoint, securityPanel);

    Component component;
    Point componentPoint;

    if (securityPanelPoint.y > 0) {
      component = securityPanel;
      componentPoint = securityPanelPoint;
    } else {
      Container contentPane =
          callRenderer.getCallContainer().getCallWindow().getFrame().getContentPane();

      Point containerPoint = SwingUtilities.convertPoint(glassPane, glassPanePoint, contentPane);

      component =
          SwingUtilities.getDeepestComponentAt(contentPane, containerPoint.x, containerPoint.y);

      componentPoint = SwingUtilities.convertPoint(contentPane, glassPanePoint, component);
    }

    if (component != null)
      component.dispatchEvent(
          new MouseEvent(
              component,
              e.getID(),
              e.getWhen(),
              e.getModifiers(),
              componentPoint.x,
              componentPoint.y,
              e.getClickCount(),
              e.isPopupTrigger()));

    e.consume();
  }

  /**
   * The handler for the security event received. The security event for starting establish a secure
   * connection.
   *
   * @param evt the security started event received
   */
  public void securityNegotiationStarted(CallPeerSecurityNegotiationStartedEvent evt) {
    if (Boolean.parseBoolean(
        GuiActivator.getResources().getSettingsString("impl.gui.PARANOIA_UI"))) {
      SrtpControl srtpControl = null;
      if (callPeer instanceof MediaAwareCallPeer) srtpControl = evt.getSecurityController();

      securityPanel = new ParanoiaTimerSecurityPanel<SrtpControl>(srtpControl);

      setSecurityPanelVisible(true);
    }
  }

  /**
   * Indicates that the security has gone off.
   *
   * @param evt the <tt>CallPeerSecurityOffEvent</tt> that notified us
   */
  public void securityOff(final CallPeerSecurityOffEvent evt) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              securityOff(evt);
            }
          });
      return;
    }

    if (evt.getSessionType() == CallPeerSecurityOffEvent.AUDIO_SESSION) {
      securityStatusLabel.setText("");
      securityStatusLabel.setSecurityOff();
      if (securityStatusLabel.getBorder() == null)
        securityStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 3));
    }

    securityPanel.securityOff(evt);
  }

  /**
   * Indicates that the security is turned on.
   *
   * <p>Sets the secured status icon to the status panel and initializes/updates the corresponding
   * security details.
   *
   * @param evt Details about the event that caused this message.
   */
  public void securityOn(final CallPeerSecurityOnEvent evt) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              securityOn(evt);
            }
          });
      return;
    }

    // If the securityOn is called without a specific event, we'll just set
    // the security label status to on.
    if (evt == null) {
      securityStatusLabel.setSecurityOn();
      return;
    }

    SrtpControl srtpControl = evt.getSecurityController();

    if (!srtpControl.requiresSecureSignalingTransport()
        || callPeer.getProtocolProvider().isSignalingTransportSecure()) {
      if (srtpControl instanceof ZrtpControl) {
        securityStatusLabel.setText("zrtp");

        if (!((ZrtpControl) srtpControl).isSecurityVerified())
          securityStatusLabel.setSecurityPending();
        else securityStatusLabel.setSecurityOn();
      } else securityStatusLabel.setSecurityOn();
    }

    // if we have some other panel, using other control
    if (!srtpControl.getClass().isInstance(securityPanel.getSecurityControl())
        || (securityPanel instanceof ParanoiaTimerSecurityPanel)) {
      setSecurityPanelVisible(false);

      securityPanel = SecurityPanel.create(this, callPeer, srtpControl);

      if (srtpControl instanceof ZrtpControl)
        ((ZrtpSecurityPanel) securityPanel).setSecurityStatusLabel(securityStatusLabel);
    }

    securityPanel.securityOn(evt);

    boolean isSecurityLowPriority =
        Boolean.parseBoolean(
            GuiActivator.getResources()
                .getSettingsString("impl.gui.I_DONT_CARE_THAT_MUCH_ABOUT_SECURITY"));

    // Display ZRTP panel in case SAS was not verified or a AOR mismtach
    // was detected during creation of ZrtpSecurityPanel.
    // Don't show panel if user does not care about security at all.
    if (srtpControl instanceof ZrtpControl
        && !isSecurityLowPriority
        && (!((ZrtpControl) srtpControl).isSecurityVerified()
            || ((ZrtpSecurityPanel) securityPanel).isZidAorMismatch())) {
      setSecurityPanelVisible(true);
    }

    this.revalidate();
  }

  /** Indicates that the security status is pending confirmation. */
  public void securityPending() {
    securityStatusLabel.setSecurityPending();
  }

  /**
   * Indicates that the security is timeouted, is not supported by the other end.
   *
   * @param evt Details about the event that caused this message.
   */
  public void securityTimeout(final CallPeerSecurityTimeoutEvent evt) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              securityTimeout(evt);
            }
          });
      return;
    }

    if (securityPanel != null) securityPanel.securityTimeout(evt);
  }

  /**
   * Sets the reason of a call failure if one occurs. The renderer should display this reason to the
   * user.
   *
   * @param reason the reason to display
   */
  public void setErrorReason(final String reason) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setErrorReason(reason);
            }
          });
      return;
    }

    if (errorMessageComponent == null) {
      errorMessageComponent = new JTextPane();

      JTextPane textPane = (JTextPane) errorMessageComponent;
      textPane.setEditable(false);
      textPane.setOpaque(false);

      StyledDocument doc = textPane.getStyledDocument();

      MutableAttributeSet standard = new SimpleAttributeSet();
      StyleConstants.setAlignment(standard, StyleConstants.ALIGN_CENTER);
      StyleConstants.setFontFamily(standard, textPane.getFont().getFamily());
      StyleConstants.setFontSize(standard, 12);
      doc.setParagraphAttributes(0, 0, standard, true);

      GridBagConstraints constraints = new GridBagConstraints();
      constraints.fill = GridBagConstraints.HORIZONTAL;
      constraints.gridx = 0;
      constraints.gridy = 4;
      constraints.weightx = 1;
      constraints.weighty = 0;
      constraints.insets = new Insets(5, 0, 0, 0);

      add(errorMessageComponent, constraints);
      this.revalidate();
    }

    errorMessageComponent.setText(reason);

    if (isVisible()) errorMessageComponent.repaint();
  }

  /**
   * Shows/hides the visual <tt>Component</tt> depicting the video streaming from the local
   * peer/user to the remote peer(s).
   *
   * @param visible <tt>true</tt> to show the visual <tt>Component</tt> depicting the video
   *     streaming from the local peer/user to the remote peer(s); <tt>false</tt>, otherwise
   */
  public void setLocalVideoVisible(boolean visible) {
    uiVideoHandler.setLocalVideoVisible(visible);
  }

  /**
   * Sets the mute status icon to the status panel.
   *
   * @param isMute indicates if the call with this peer is muted
   */
  public void setMute(final boolean isMute) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setMute(isMute);
            }
          });
      return;
    }

    if (isMute) {
      muteStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON)));
      muteStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 3, 2, 3));
    } else {
      muteStatusLabel.setIcon(null);
      muteStatusLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
    }

    // Update input volume control button state to reflect the current
    // mute status.
    if (localLevel.isSelected() != isMute) localLevel.setSelected(isMute);

    this.revalidate();
    this.repaint();
  }

  /**
   * Sets the "on hold" property value.
   *
   * @param isOnHold indicates if the call with this peer is put on hold
   */
  public void setOnHold(final boolean isOnHold) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setOnHold(isOnHold);
            }
          });
      return;
    }

    if (isOnHold) {
      holdStatusLabel.setIcon(new ImageIcon(ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON)));
      holdStatusLabel.setBorder(BorderFactory.createEmptyBorder(2, 3, 2, 3));
    } else {
      holdStatusLabel.setIcon(null);
      holdStatusLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
    }

    this.revalidate();
    this.repaint();
  }

  /**
   * Set the image of the peer
   *
   * @param image new image
   */
  public void setPeerImage(byte[] image) {
    // If the image is still null we try to obtain it from one of the
    // available contact sources.
    if (image == null || image.length <= 0) {
      GuiActivator.getContactList().setSourceContactImage(peerName, photoLabel, 100, 100);
    } else {
      peerImage = ImageUtils.getScaledRoundedIcon(image, 100, 100);
      if (peerImage == null) peerImage = getPhotoLabelIcon();

      if (!SwingUtilities.isEventDispatchThread()) {
        SwingUtilities.invokeLater(
            new Runnable() {
              public void run() {
                photoLabel.setIcon(peerImage);
                photoLabel.repaint();
              }
            });
      } else {
        photoLabel.setIcon(peerImage);
        photoLabel.repaint();
      }
    }
  }

  /**
   * Sets the name of the peer.
   *
   * @param name the name of the peer
   */
  public void setPeerName(final String name) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setPeerName(name);
            }
          });
      return;
    }

    peerName = name;

    ((OneToOneCallPanel) callRenderer).setPeerName(name);
  }

  /**
   * Sets the state of the contained call peer by specifying the state name.
   *
   * @param oldState the previous state of the peer
   * @param newState the new state of the peer
   * @param stateString the state of the contained call peer
   */
  public void setPeerState(
      final CallPeerState oldState, final CallPeerState newState, final String stateString) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setPeerState(oldState, newState, stateString);
            }
          });
      return;
    }

    this.callStatusLabel.setText(stateString);

    if (newState == CallPeerState.CONNECTED
        && !CallPeerState.isOnHold(oldState)
        && !securityStatusLabel.isSecurityStatusSet()) {
      securityStatusLabel.setSecurityOff();
    }
  }

  /**
   * Shows/hides the security panel.
   *
   * @param isVisible <tt>true</tt> to show the security panel, <tt>false</tt> to hide it
   */
  public void setSecurityPanelVisible(final boolean isVisible) {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(
          new Runnable() {
            public void run() {
              setSecurityPanelVisible(isVisible);
            }
          });
      return;
    }

    final JFrame callFrame = callRenderer.getCallContainer().getCallWindow().getFrame();

    final JPanel glassPane = (JPanel) callFrame.getGlassPane();

    if (!isVisible) {
      // Need to hide the security panel explicitly in order to keep the
      // fade effect.
      securityPanel.setVisible(false);
      glassPane.setVisible(false);
      glassPane.removeAll();
    } else {
      glassPane.setLayout(null);
      glassPane.addMouseListener(
          new MouseListener() {
            public void mouseClicked(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mouseEntered(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mouseExited(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mousePressed(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }

            public void mouseReleased(MouseEvent e) {
              redispatchMouseEvent(glassPane, e);
            }
          });

      Point securityLabelPoint = securityStatusLabel.getLocation();

      Point newPoint =
          SwingUtilities.convertPoint(
              securityStatusLabel.getParent(),
              securityLabelPoint.x,
              securityLabelPoint.y,
              callFrame);

      securityPanel.setBeginPoint(new Point((int) newPoint.getX() + 15, 0));
      securityPanel.setBounds(0, (int) newPoint.getY() - 5, this.getWidth(), 130);

      glassPane.add(securityPanel);
      // Need to show the security panel explicitly in order to keep the
      // fade effect.
      securityPanel.setVisible(true);
      glassPane.setVisible(true);

      glassPane.addComponentListener(
          new ComponentAdapter() {
            /** Invoked when the component's size changes. */
            @Override
            public void componentResized(ComponentEvent e) {
              if (glassPane.isVisible()) {
                glassPane.setVisible(false);
                callFrame.removeComponentListener(this);
              }
            }
          });
    }
  }

  /**
   * Updates this view i.e. <tt>OneToOneCallPeerPanel</tt> so that it depicts the current state of
   * its model i.e. <tt>callPeer</tt>.
   */
  private void updateViewFromModel() {
    /*
     * We receive events/notifications from various threads and we respond
     * to them in the AWT event dispatching thread. It is possible to first
     * schedule an event to be brought to the AWT event dispatching thread,
     * then to have #dispose() invoked on this instance and, finally, to
     * receive the scheduled event in the AWT event dispatching thread. In
     * such a case, this disposed instance should not respond to the event
     * because it may, for example, steal a visual Components depicting
     * video (which cannot belong to more than one parent at a time) from
     * another non-disposed OneToOneCallPeerPanel.
     */
    if (!disposed) {
      if (SwingUtilities.isEventDispatchThread()) updateViewFromModelInEventDispatchThread();
      else {
        SwingUtilities.invokeLater(updateViewFromModelInEventDispatchThread);
      }
    }
  }

  /**
   * Updates this view i.e. <tt>OneToOneCallPeerPanel</tt> so that it depicts the current state of
   * its model i.e. <tt>callPeer</tt>. The update is performed in the AWT event dispatching thread.
   */
  private void updateViewFromModelInEventDispatchThread() {
    /*
     * We receive events/notifications from various threads and we respond
     * to them in the AWT event dispatching thread. It is possible to first
     * schedule an event to be brought to the AWT event dispatching thread,
     * then to have #dispose() invoked on this instance and, finally, to
     * receive the scheduled event in the AWT event dispatching thread. In
     * such a case, this disposed instance should not respond to the event
     * because it may, for example, steal a visual Components depicting
     * video (which cannot belong to more than one parent at a time) from
     * another non-disposed OneToOneCallPeerPanel.
     */
    if (disposed) return;

    /*
     * Update the display of visual <tt>Component</tt>s depicting video
     * streaming between the local peer/user and the remote peer(s).
     */

    OperationSetVideoTelephony videoTelephony =
        callPeer.getProtocolProvider().getOperationSet(OperationSetVideoTelephony.class);
    Component remoteVideo = null;
    Component localVideo = null;

    if (videoTelephony != null) {
      List<Component> remoteVideos = videoTelephony.getVisualComponents(callPeer);

      if ((remoteVideos != null) && !remoteVideos.isEmpty()) {
        /*
         * TODO OneToOneCallPeerPanel displays a one-to-one conversation
         * between the local peer/user and a specific remote peer. If
         * the remote peer is the focus of a telephony conference of its
         * own, it may be sending multiple videos to the local peer.
         * Switching to a user interface which displays multiple videos
         * is the responsibility of whoever decided that this
         * OneToOneCallPeerPanel is to be used to depict the current
         * state of the CallConference associated with the CallPeer
         * depicted by this instance. If that switching decides that
         * this instance is to continue being the user interface, then
         * we should probably pick up the remote video which is
         * generated by the remote peer and not one of its
         * ConferenceMembers.
         */
        remoteVideo = remoteVideos.get(0);
      }

      if (uiVideoHandler.isLocalVideoVisible()) {
        try {
          localVideo = videoTelephony.getLocalVisualComponent(callPeer);
        } catch (OperationFailedException ofe) {
          /*
           * Well, we cannot do much about the exception. We'll just
           * not display the local video.
           */
          logger.warn("Failed to retrieve local video to be displayed.", ofe);
        }
      }

      /*
       * Determine whether there is actually a change in the local and
       * remote videos which requires an update.
       */
      boolean localVideoChanged =
          ((localVideo != this.localVideo)
              || ((localVideo != null) && !UIVideoHandler2.isAncestor(center, localVideo)));
      boolean remoteVideoChanged =
          ((remoteVideo != this.remoteVideo)
              || ((remoteVideo != null) && !UIVideoHandler2.isAncestor(center, remoteVideo)));

      // If the remote video has changed, maybe the CallPanel can display
      // the LO/SD/HD button.
      if (remoteVideoChanged) {
        // Updates video component which may listen the mouse and key
        // events.
        if (desktopSharingMouseAndKeyboardListener != null) {
          desktopSharingMouseAndKeyboardListener.setVideoComponent(remoteVideo);
        }

        CallPanel callPanel = callRenderer.getCallContainer();
        // The remote video has been added, then tries to display the
        // LO/SD/HD button.
        if (remoteVideo != null) {
          callPanel.addRemoteVideoSpecificComponents(callPeer);
        }
        // The remote video has been removed, then hide the LO/SD/HD
        // button if it is currently displayed.
        else {
          callPanel.removeRemoteVideoSpecificComponents();
        }
      }

      if (localVideoChanged || remoteVideoChanged) {
        /*
         * VideoContainer and JAWTRenderer cannot handle random
         * additions of Components. Removing the localVideo when the
         * user has requests its hiding though, should work without
         * removing all Components from the VideoCotainer and adding
         * them again.
         */
        if (localVideoChanged && !remoteVideoChanged && (localVideo == null)) {
          if (this.localVideo != null) {
            center.remove(this.localVideo);
            this.localVideo = null;

            if (closeLocalVisualComponentButton != null)
              center.remove(closeLocalVisualComponentButton);
          }
        } else {
          center.removeAll();
          this.localVideo = null;
          this.remoteVideo = null;

          /*
           * AWT does not make a guarantee about the Z order even
           * within an operating system i.e. the order of adding the
           * Components to their Container does not mean that they
           * will be determinedly painted in that or reverse order.
           * Anyway, there appears to be an expectation among the
           * developers less acquainted with AWT that AWT paints the
           * Components of a Container in an order that is the reverse
           * of the order of their adding. In order to satisfy that
           * expectation and thus give at least some idea to the
           * developers reading the code bellow, do add the Components
           * according to that expectation.
           */

          if (localVideo != null) {
            if (closeLocalVisualComponentButton == null) {
              closeLocalVisualComponentButton = new CloseLocalVisualComponentButton(uiVideoHandler);
            }
            center.add(closeLocalVisualComponentButton, VideoLayout.CLOSE_LOCAL_BUTTON, -1);

            center.add(localVideo, VideoLayout.LOCAL, -1);
            this.localVideo = localVideo;
          }

          if (remoteVideo != null) {
            center.add(remoteVideo, VideoLayout.CENTER_REMOTE, -1);
            this.remoteVideo = remoteVideo;
          }
        }
      }
    }
  }

  /** The <tt>TransparentPanel</tt> that will display the peer status. */
  private static class PeerStatusPanel extends TransparentPanel {
    /**
     * Silence the serial warning. Though there isn't a plan to serialize the instances of the
     * class, there're no fields so the default serialization routine will work.
     */
    private static final long serialVersionUID = 0L;

    /**
     * Constructs a new <tt>PeerStatusPanel</tt>.
     *
     * @param layout the <tt>LayoutManager</tt> to use
     */
    public PeerStatusPanel(LayoutManager layout) {
      super(layout);
    }

    /** @{inheritDoc} */
    @Override
    public void paintComponent(Graphics g) {
      super.paintComponent(g);

      g = g.create();

      try {
        AntialiasingManager.activateAntialiasing(g);

        g.setColor(Color.DARK_GRAY);
        g.fillRoundRect(0, 0, this.getWidth(), this.getHeight(), 10, 10);
      } finally {
        g.dispose();
      }
    }
  }
}
/** This class starts/stops/monitors the persistent requests on Freenet 0.7. */
public class PersistenceManager implements IFcpPersistentRequestsHandler {

  // FIXME    Problem: positiv abgleich klappt, aber woher weiss ich wann LIST durch ist um zu
  // checken ob welche fehlen?

  private static final Logger logger = Logger.getLogger(PersistenceManager.class.getName());

  // this would belong to the models, but its not needed there without persistence, hence we
  // maintain it here
  private final Hashtable<String, FrostUploadItem> uploadModelItems =
      new Hashtable<String, FrostUploadItem>();
  private final Hashtable<String, FrostDownloadItem> downloadModelItems =
      new Hashtable<String, FrostDownloadItem>();

  private final UploadModel uploadModel;
  private final DownloadModel downloadModel;

  private final DirectTransferQueue directTransferQueue;
  private final DirectTransferThread directTransferThread;

  private boolean showExternalItemsDownload;
  private boolean showExternalItemsUpload;

  private boolean isConnected = true; // we start in connected state

  private final FcpPersistentQueue persistentQueue;
  private final FcpListenThreadConnection fcpConn;
  private final FcpMultiRequestConnectionFileTransferTools fcpTools;

  private final Set<String> directGETsInProgress = new HashSet<String>();
  private final Set<String> directPUTsInProgress = new HashSet<String>();

  private final Set<String> directPUTsWithoutAnswer = new HashSet<String>();

  /** @return true if Frost is configured to use persistent uploads and downloads, false if not */
  public static boolean isPersistenceEnabled() {
    return Core.frostSettings.getBoolValue(SettingsClass.FCP2_USE_PERSISTENCE);
  }

  /** Must be called after the upload and download model is initialized! */
  public PersistenceManager(final UploadModel um, final DownloadModel dm) throws Throwable {

    showExternalItemsDownload =
        Core.frostSettings.getBoolValue(SettingsClass.GQ_SHOW_EXTERNAL_ITEMS_DOWNLOAD);
    showExternalItemsUpload =
        Core.frostSettings.getBoolValue(SettingsClass.GQ_SHOW_EXTERNAL_ITEMS_UPLOAD);

    if (FcpHandler.inst().getFreenetNode() == null) {
      throw new Exception("No freenet nodes defined");
    }
    final NodeAddress na = FcpHandler.inst().getFreenetNode();
    fcpConn = FcpListenThreadConnection.createInstance(na);
    fcpTools = new FcpMultiRequestConnectionFileTransferTools(fcpConn);

    Core.frostSettings.addPropertyChangeListener(
        new PropertyChangeListener() {
          public void propertyChange(final PropertyChangeEvent evt) {
            if (evt.getPropertyName().equals(SettingsClass.GQ_SHOW_EXTERNAL_ITEMS_DOWNLOAD)) {
              showExternalItemsDownload =
                  Core.frostSettings.getBoolValue(SettingsClass.GQ_SHOW_EXTERNAL_ITEMS_DOWNLOAD);
              if (showExternalItemsDownload) {
                // get external items
                showExternalDownloadItems();
              }
            } else if (evt.getPropertyName().equals(SettingsClass.GQ_SHOW_EXTERNAL_ITEMS_UPLOAD)) {
              showExternalItemsUpload =
                  Core.frostSettings.getBoolValue(SettingsClass.GQ_SHOW_EXTERNAL_ITEMS_UPLOAD);
              if (showExternalItemsUpload) {
                // get external items
                showExternalUploadItems();
              }
            }
          }
        });

    uploadModel = um;
    downloadModel = dm;

    // initially get all items from model
    for (int x = 0; x < uploadModel.getItemCount(); x++) {
      final FrostUploadItem ul = (FrostUploadItem) uploadModel.getItemAt(x);
      if (ul.getGqIdentifier() != null) {
        uploadModelItems.put(ul.getGqIdentifier(), ul);
      }
    }
    for (int x = 0; x < downloadModel.getItemCount(); x++) {
      final FrostDownloadItem ul = (FrostDownloadItem) downloadModel.getItemAt(x);
      if (ul.getGqIdentifier() != null) {
        downloadModelItems.put(ul.getGqIdentifier(), ul);
      }
    }

    // enqueue listeners to keep updated about the model items
    uploadModel.addOrderedModelListener(
        new SortedModelListener<FrostUploadItem>() {
          public void modelCleared() {
            for (final FrostUploadItem ul : uploadModelItems.values()) {
              if (ul.isExternal() == false) {
                fcpTools.removeRequest(ul.getGqIdentifier());
              }
            }
            uploadModelItems.clear();
          }

          public void itemAdded(final int position, final FrostUploadItem item) {
            uploadModelItems.put(item.getGqIdentifier(), item);
            if (!item.isExternal()) {
              // maybe start immediately
              startNewUploads();
            }
          }

          public void itemChanged(final int position, final FrostUploadItem item) {}

          public void itemsRemoved(final int[] positions, final List<FrostUploadItem> items) {
            for (final FrostUploadItem item : items) {
              uploadModelItems.remove(item.getGqIdentifier());
              if (item.isExternal() == false) {
                fcpTools.removeRequest(item.getGqIdentifier());
              }
            }
          }
        });

    downloadModel.addOrderedModelListener(
        new SortedModelListener<FrostDownloadItem>() {
          public void modelCleared() {
            for (final FrostDownloadItem ul : downloadModelItems.values()) {
              if (ul.isExternal() == false) {
                fcpTools.removeRequest(ul.getGqIdentifier());
              }
            }
            downloadModelItems.clear();
          }

          public void itemAdded(final int position, final FrostDownloadItem item) {
            downloadModelItems.put(item.getGqIdentifier(), item);
            if (!item.isExternal()) {
              // maybe start immediately
              startNewDownloads();
            }
          }

          public void itemChanged(final int position, final FrostDownloadItem item) {}

          public void itemsRemoved(final int[] positions, final List<FrostDownloadItem> items) {
            for (final FrostDownloadItem item : items) {
              downloadModelItems.remove(item.getGqIdentifier());
              if (item.isExternal() == false) {
                fcpTools.removeRequest(item.getGqIdentifier());
              }
            }
          }
        });

    directTransferQueue = new DirectTransferQueue();
    directTransferThread = new DirectTransferThread();

    persistentQueue = new FcpPersistentQueue(fcpTools, this);
  }

  public FcpMultiRequestConnectionFileTransferTools getFcpTools() {
    return fcpTools;
  }

  public void startThreads() {
    directTransferThread.start();
    persistentQueue.startThreads();
    final TimerTask task =
        new TimerTask() {
          @Override
          public void run() {
            maybeStartRequests();
          }
        };
    Core.schedule(task, 3000, 3000);
  }

  public void removeRequests(final List<String> requests) {
    for (final String id : requests) {
      fcpTools.removeRequest(id);
    }
  }

  /**
   * @param dlItem items whose global identifier is to check
   * @return true if this item is currently in the global queue, no matter in what state
   */
  public boolean isItemInGlobalQueue(final FrostDownloadItem dlItem) {
    return persistentQueue.isIdInGlobalQueue(dlItem.getGqIdentifier());
  }
  /**
   * @param ulItem items whose global identifier is to check
   * @return true if this item is currently in the global queue, no matter in what state
   */
  public boolean isItemInGlobalQueue(final FrostUploadItem ulItem) {
    return persistentQueue.isIdInGlobalQueue(ulItem.getGqIdentifier());
  }

  /**
   * Periodically check if we could start a new request. This could be done better if we check if a
   * request finished, but later...
   */
  private void maybeStartRequests() {
    // start new requests
    startNewUploads();
    startNewDownloads();
  }

  public void connected() {
    isConnected = true;
    MainFrame.getInstance().setConnected();
    logger.severe("now connected");
  }

  public void disconnected() {
    isConnected = false;

    MainFrame.getInstance().setDisconnected();

    SwingUtilities.invokeLater(
        new Runnable() {
          public void run() {
            uploadModel.removeExternalUploads();
            downloadModel.removeExternalDownloads();
          }
        });
    logger.severe("disconnected!");
  }

  public boolean isConnected() {
    return isConnected;
  }

  /**
   * Enqueue a direct GET if not already enqueued, or already downloaded to download dir.
   *
   * @return true if item was enqueued
   */
  public boolean maybeEnqueueDirectGet(
      final FrostDownloadItem dlItem, final long expectedFileSize) {
    if (!isDirectTransferInProgress(dlItem)) {
      final File targetFile = new File(dlItem.getDownloadFilename());
      if (!targetFile.isFile() || targetFile.length() != expectedFileSize) {
        directTransferQueue.appendItemToQueue(dlItem);
        return true;
      }
    }
    return false;
  }

  private void applyPriority(final FrostDownloadItem dlItem, final FcpPersistentGet getReq) {
    // apply externally changed priority
    if (dlItem.getPriority() != getReq.getPriority()) {
      if (Core.frostSettings.getBoolValue(SettingsClass.FCP2_ENFORCE_FROST_PRIO_FILE_DOWNLOAD)) {
        // reset priority with our current value
        fcpTools.changeRequestPriority(getReq.getIdentifier(), dlItem.getPriority());
      } else {
        // apply to downloaditem
        dlItem.setPriority(getReq.getPriority());
      }
    }
  }

  /** Apply the states of FcpRequestGet to the FrostDownloadItem. */
  private void applyState(final FrostDownloadItem dlItem, final FcpPersistentGet getReq) {
    // when cancelled and we expect this, don't set failed; don't even set the old priority!
    if (dlItem.isInternalRemoveExpected() && getReq.isFailed()) {
      final int returnCode = getReq.getCode();
      if (returnCode == 25) {
        return;
      }
    }

    applyPriority(dlItem, getReq);

    if (dlItem.isDirect() != getReq.isDirect()) {
      dlItem.setDirect(getReq.isDirect());
    }

    if (!getReq.isProgressSet() && !getReq.isSuccess() && !getReq.isFailed()) {
      if (dlItem.getState() == FrostDownloadItem.STATE_WAITING) {
        dlItem.setState(FrostDownloadItem.STATE_PROGRESS);
      }
      return;
    }

    if (getReq.isProgressSet()) {
      final int doneBlocks = getReq.getDoneBlocks();
      final int requiredBlocks = getReq.getRequiredBlocks();
      final int totalBlocks = getReq.getTotalBlocks();
      final boolean isFinalized = getReq.isFinalized();
      if (totalBlocks > 0) {
        dlItem.setDoneBlocks(doneBlocks);
        dlItem.setRequiredBlocks(requiredBlocks);
        dlItem.setTotalBlocks(totalBlocks);
        dlItem.setFinalized(isFinalized);
        dlItem.fireValueChanged();
      }
      if (dlItem.getState() != FrostDownloadItem.STATE_PROGRESS) {
        dlItem.setState(FrostDownloadItem.STATE_PROGRESS);
      }
    }
    if (getReq.isSuccess()) {
      // maybe progress was not completely sent
      dlItem.setFinalized(true);
      if (dlItem.getTotalBlocks() > 0 && dlItem.getDoneBlocks() < dlItem.getRequiredBlocks()) {
        dlItem.setDoneBlocks(dlItem.getRequiredBlocks());
        dlItem.fireValueChanged();
      }
      if (dlItem.isExternal()) {
        dlItem.setFileSize(getReq.getFilesize());
        dlItem.setState(FrostDownloadItem.STATE_DONE);
      } else {
        if (dlItem.isDirect()) {
          maybeEnqueueDirectGet(dlItem, getReq.getFilesize());
        } else {
          final FcpResultGet result = new FcpResultGet(true);
          final File targetFile = new File(dlItem.getDownloadFilename());
          FileTransferManager.inst()
              .getDownloadManager()
              .notifyDownloadFinished(dlItem, result, targetFile);
        }
      }
    }
    if (getReq.isFailed()) {
      final String desc = getReq.getCodeDesc();
      if (dlItem.isExternal()) {
        dlItem.setState(FrostDownloadItem.STATE_FAILED);
        dlItem.setErrorCodeDescription(desc);
      } else {
        final int returnCode = getReq.getCode();
        final boolean isFatal = getReq.isFatal();

        final String redirectURI = getReq.getRedirectURI();
        final FcpResultGet result = new FcpResultGet(false, returnCode, desc, isFatal, redirectURI);
        final File targetFile = new File(dlItem.getDownloadFilename());
        final boolean retry =
            FileTransferManager.inst()
                .getDownloadManager()
                .notifyDownloadFinished(dlItem, result, targetFile);
        if (retry) {
          fcpTools.removeRequest(getReq.getIdentifier());
          startDownload(dlItem); // restart immediately
        }
      }
    }
  }

  /** Apply the states of FcpRequestPut to the FrostUploadItem. */
  private void applyState(final FrostUploadItem ulItem, final FcpPersistentPut putReq) {

    // when cancelled and we expect this, don't set failed; don't even set the old priority!
    if (ulItem.isInternalRemoveExpected() && putReq.isFailed()) {
      final int returnCode = putReq.getCode();
      if (returnCode == 25) {
        return;
      }
    }

    if (directPUTsWithoutAnswer.contains(ulItem.getGqIdentifier())) {
      // we got an answer
      directPUTsWithoutAnswer.remove(ulItem.getGqIdentifier());
    }

    // apply externally changed priority
    if (ulItem.getPriority() != putReq.getPriority()) {
      if (Core.frostSettings.getBoolValue(SettingsClass.FCP2_ENFORCE_FROST_PRIO_FILE_UPLOAD)) {
        // reset priority with our current value
        fcpTools.changeRequestPriority(putReq.getIdentifier(), ulItem.getPriority());
      } else {
        // apply to uploaditem
        ulItem.setPriority(putReq.getPriority());
      }
    }

    if (!putReq.isProgressSet() && !putReq.isSuccess() && !putReq.isFailed()) {
      if (ulItem.getState() == FrostUploadItem.STATE_WAITING) {
        ulItem.setState(FrostUploadItem.STATE_PROGRESS);
      }
      return;
    }

    if (putReq.isProgressSet()) {
      final int doneBlocks = putReq.getDoneBlocks();
      final int totalBlocks = putReq.getTotalBlocks();
      final boolean isFinalized = putReq.isFinalized();
      if (totalBlocks > 0) {
        ulItem.setDoneBlocks(doneBlocks);
        ulItem.setTotalBlocks(totalBlocks);
        ulItem.setFinalized(isFinalized);
        ulItem.fireValueChanged();
      }
      if (ulItem.getState() != FrostUploadItem.STATE_PROGRESS) {
        ulItem.setState(FrostUploadItem.STATE_PROGRESS);
      }
    }
    if (putReq.isSuccess()) {
      // maybe progress was not completely sent
      ulItem.setFinalized(true);
      if (ulItem.getTotalBlocks() > 0 && ulItem.getDoneBlocks() != ulItem.getTotalBlocks()) {
        ulItem.setDoneBlocks(ulItem.getTotalBlocks());
      }
      final String chkKey = putReq.getUri();
      if (ulItem.isExternal()) {
        ulItem.setState(FrostUploadItem.STATE_DONE);
        ulItem.setKey(chkKey);
      } else {
        final FcpResultPut result = new FcpResultPut(FcpResultPut.Success, chkKey);
        FileTransferManager.inst().getUploadManager().notifyUploadFinished(ulItem, result);
      }
    }
    if (putReq.isFailed()) {
      final String desc = putReq.getCodeDesc();
      if (ulItem.isExternal()) {
        ulItem.setState(FrostUploadItem.STATE_FAILED);
        ulItem.setErrorCodeDescription(desc);
      } else {
        final int returnCode = putReq.getCode();
        final boolean isFatal = putReq.isFatal();

        final FcpResultPut result;
        if (returnCode == 9) {
          result = new FcpResultPut(FcpResultPut.KeyCollision, returnCode, desc, isFatal);
        } else if (returnCode == 5) {
          result = new FcpResultPut(FcpResultPut.Retry, returnCode, desc, isFatal);
        } else {
          result = new FcpResultPut(FcpResultPut.Error, returnCode, desc, isFatal);
        }
        FileTransferManager.inst().getUploadManager().notifyUploadFinished(ulItem, result);
      }
    }
  }

  private void startNewUploads() {
    boolean isLimited = true;
    int currentAllowedUploadCount = 0;
    {
      final int allowedConcurrentUploads =
          Core.frostSettings.getIntValue(SettingsClass.UPLOAD_MAX_THREADS);
      if (allowedConcurrentUploads <= 0) {
        isLimited = false;
      } else {
        int runningUploads = 0;
        for (final FrostUploadItem ulItem : uploadModelItems.values()) {
          if (!ulItem.isExternal() && ulItem.getState() == FrostUploadItem.STATE_PROGRESS) {
            runningUploads++;
          }
        }
        currentAllowedUploadCount = allowedConcurrentUploads - runningUploads;
        if (currentAllowedUploadCount < 0) {
          currentAllowedUploadCount = 0;
        }
      }
    }
    {
      while (!isLimited || currentAllowedUploadCount > 0) {
        final FrostUploadItem ulItem =
            FileTransferManager.inst().getUploadManager().selectNextUploadItem();
        if (ulItem == null) {
          break;
        }
        if (startUpload(ulItem)) {
          currentAllowedUploadCount--;
        }
      }
    }
  }

  public boolean startUpload(final FrostUploadItem ulItem) {
    if (ulItem == null || ulItem.getState() != FrostUploadItem.STATE_WAITING) {
      return false;
    }

    ulItem.setUploadStartedMillis(System.currentTimeMillis());

    ulItem.setState(FrostUploadItem.STATE_PROGRESS);

    // start the upload
    final boolean doMime;
    final boolean setTargetFileName;
    if (ulItem.isSharedFile()) {
      doMime = false;
      setTargetFileName = false;
    } else {
      doMime = true;
      setTargetFileName = true;
    }

    // try to start using DDA
    boolean isDda =
        fcpTools.startPersistentPutUsingDda(
            ulItem.getGqIdentifier(),
            ulItem.getFile(),
            ulItem.getFileName(),
            doMime,
            setTargetFileName,
            ulItem.getCompress(),
            ulItem.getFreenetCompatibilityMode(),
            ulItem.getPriority());

    if (!isDda) {
      // upload was not startet because DDA is not allowed...
      // if UploadManager selected this file then it is not already in progress!
      directTransferQueue.appendItemToQueue(ulItem);
    }
    return true;
  }

  private void startNewDownloads() {
    boolean isLimited = true;
    int currentAllowedDownloadCount = 0;
    {
      final int allowedConcurrentDownloads =
          Core.frostSettings.getIntValue(SettingsClass.DOWNLOAD_MAX_THREADS);
      if (allowedConcurrentDownloads <= 0) {
        isLimited = false;
      } else {
        int runningDownloads = 0;
        for (final FrostDownloadItem dlItem : downloadModelItems.values()) {
          if (!dlItem.isExternal() && dlItem.getState() == FrostDownloadItem.STATE_PROGRESS) {
            runningDownloads++;
          }
        }
        currentAllowedDownloadCount = allowedConcurrentDownloads - runningDownloads;
        if (currentAllowedDownloadCount < 0) {
          currentAllowedDownloadCount = 0;
        }
      }
    }
    {
      while (!isLimited || currentAllowedDownloadCount > 0) {
        final FrostDownloadItem dlItem =
            FileTransferManager.inst().getDownloadManager().selectNextDownloadItem();
        if (dlItem == null) {
          break;
        }
        // start the download
        if (startDownload(dlItem)) {
          currentAllowedDownloadCount--;
        }
      }
    }
  }

  public boolean startDownload(final FrostDownloadItem dlItem) {

    if (dlItem == null || dlItem.getState() != FrostDownloadItem.STATE_WAITING) {
      return false;
    }

    dlItem.setDownloadStartedTime(System.currentTimeMillis());

    dlItem.setState(FrostDownloadItem.STATE_PROGRESS);

    final String gqid = dlItem.getGqIdentifier();
    final File targetFile = new File(dlItem.getDownloadFilename());
    boolean isDda =
        fcpTools.startPersistentGet(dlItem.getKey(), gqid, targetFile, dlItem.getPriority());
    dlItem.setDirect(!isDda);

    return true;
  }

  private void showExternalUploadItems() {
    final Map<String, FcpPersistentPut> items = persistentQueue.getUploadRequests();
    for (final FcpPersistentPut uploadRequest : items.values()) {
      if (!uploadModelItems.containsKey(uploadRequest.getIdentifier())) {
        addExternalItem(uploadRequest);
      }
    }
  }

  private void showExternalDownloadItems() {
    final Map<String, FcpPersistentGet> items = persistentQueue.getDownloadRequests();
    for (final FcpPersistentGet downloadRequest : items.values()) {
      if (!downloadModelItems.containsKey(downloadRequest.getIdentifier())) {
        addExternalItem(downloadRequest);
      }
    }
  }

  private void addExternalItem(final FcpPersistentPut uploadRequest) {
    final FrostUploadItem ulItem = new FrostUploadItem();
    ulItem.setGqIdentifier(uploadRequest.getIdentifier());
    ulItem.setExternal(true);
    // direct uploads maybe have no filename, use identifier
    String fileName = uploadRequest.getFilename();
    if (fileName == null) {
      fileName = uploadRequest.getIdentifier();
    } else if (fileName.indexOf('/') > -1 || fileName.indexOf('\\') > -1) {
      // filename contains directories, use only filename
      final String stmp = new File(fileName).getName();
      if (stmp.length() > 0) {
        fileName = stmp; // use plain filename
      }
    }
    ulItem.setFile(new File(fileName));
    ulItem.setFileName(fileName);
    ulItem.setFileSize(uploadRequest.getFileSize());
    ulItem.setPriority(uploadRequest.getPriority());

    ulItem.setState(FrostUploadItem.STATE_PROGRESS);
    SwingUtilities.invokeLater(
        new Runnable() {
          public void run() {
            uploadModel.addExternalItem(ulItem);
          }
        });
    applyState(ulItem, uploadRequest);
  }

  private void addExternalItem(final FcpPersistentGet downloadRequest) {
    // direct downloads maybe have no filename, use identifier
    String fileName = downloadRequest.getFilename();
    if (fileName == null) {
      fileName = downloadRequest.getIdentifier();
    } else if (fileName.indexOf('/') > -1 || fileName.indexOf('\\') > -1) {
      // filename contains directories, use only filename
      final String stmp = new File(fileName).getName();
      if (stmp.length() > 0) {
        fileName = stmp; // use plain filename
      }
    }
    final FrostDownloadItem dlItem = new FrostDownloadItem(fileName, downloadRequest.getUri());
    dlItem.setExternal(true);
    dlItem.setGqIdentifier(downloadRequest.getIdentifier());
    dlItem.setState(FrostDownloadItem.STATE_PROGRESS);
    SwingUtilities.invokeLater(
        new Runnable() {
          public void run() {
            downloadModel.addExternalItem(dlItem);
          }
        });
    applyState(dlItem, downloadRequest);
  }

  public boolean isDirectTransferInProgress(final FrostDownloadItem dlItem) {
    final String id = dlItem.getGqIdentifier();
    return directGETsInProgress.contains(id);
  }

  public boolean isDirectTransferInProgress(final FrostUploadItem ulItem) {
    final String id = ulItem.getGqIdentifier();
    if (directPUTsInProgress.contains(id)) {
      return true;
    }
    if (directPUTsWithoutAnswer.contains(id)) {
      return true;
    }
    return false;
  }

  private class DirectTransferThread extends Thread {

    @Override
    public void run() {

      final int maxAllowedExceptions = 5;
      int catchedExceptions = 0;

      while (true) {
        try {
          // if there is no work in queue this call waits for a new queue item
          final ModelItem<?> item = directTransferQueue.getItemFromQueue();

          if (item == null) {
            // paranoia, should never happen
            Mixed.wait(5 * 1000);
            continue;
          }

          if (item instanceof FrostUploadItem) {
            // transfer bytes to node
            final FrostUploadItem ulItem = (FrostUploadItem) item;
            // FIXME: provide item, state=Transfer to node, % shows progress
            final String gqid = ulItem.getGqIdentifier();
            final boolean doMime;
            final boolean setTargetFileName;
            if (ulItem.isSharedFile()) {
              doMime = false;
              setTargetFileName = false;
            } else {
              doMime = true;
              setTargetFileName = true;
            }
            final NodeMessage answer =
                fcpTools.startDirectPersistentPut(
                    gqid,
                    ulItem.getFile(),
                    ulItem.getFileName(),
                    doMime,
                    setTargetFileName,
                    ulItem.getCompress(),
                    ulItem.getFreenetCompatibilityMode(),
                    ulItem.getPriority());
            if (answer == null) {
              final String desc = "Could not open a new FCP2 socket for direct put!";
              final FcpResultPut result = new FcpResultPut(FcpResultPut.Error, -1, desc, false);
              FileTransferManager.inst().getUploadManager().notifyUploadFinished(ulItem, result);

              logger.severe(desc);
            } else {
              // wait for an answer, don't start request again
              directPUTsWithoutAnswer.add(gqid);
            }

            directPUTsInProgress.remove(gqid);

          } else if (item instanceof FrostDownloadItem) {
            // transfer bytes from node
            final FrostDownloadItem dlItem = (FrostDownloadItem) item;
            // FIXME: provide item, state=Transfer from node, % shows progress
            final String gqid = dlItem.getGqIdentifier();
            final File targetFile = new File(dlItem.getDownloadFilename());

            final boolean retryNow;
            NodeMessage answer = null;

            try {
              answer = fcpTools.startDirectPersistentGet(gqid, targetFile);
            } catch (final FileNotFoundException e) {
              final String msg =
                  "Could not write to " + dlItem.getDownloadFilename() + ": " + e.getMessage();
              System.out.println(msg);
              logger.severe(msg);
            }

            if (answer != null) {
              final FcpResultGet result = new FcpResultGet(true);
              FileTransferManager.inst()
                  .getDownloadManager()
                  .notifyDownloadFinished(dlItem, result, targetFile);
              retryNow = false;
            } else {
              logger.severe("Could not open a new fcp socket for direct get!");
              final FcpResultGet result = new FcpResultGet(false);
              retryNow =
                  FileTransferManager.inst()
                      .getDownloadManager()
                      .notifyDownloadFinished(dlItem, result, targetFile);
            }

            directGETsInProgress.remove(gqid);

            if (retryNow) {
              startDownload(dlItem);
            }
          }

        } catch (final Throwable t) {
          logger.log(Level.SEVERE, "Exception catched", t);
          catchedExceptions++;
        }

        if (catchedExceptions > maxAllowedExceptions) {
          logger.log(Level.SEVERE, "Stopping DirectTransferThread because of too much exceptions");
          break;
        }
      }
    }
  }

  /**
   * A queue class that queues items waiting for its direct transfer (put to node or get from node).
   */
  private class DirectTransferQueue {

    private final LinkedList<ModelItem<?>> queue = new LinkedList<ModelItem<?>>();

    public synchronized ModelItem<?> getItemFromQueue() {
      try {
        // let dequeueing threads wait for work
        while (queue.isEmpty()) {
          wait();
        }
      } catch (final InterruptedException e) {
        return null; // waiting abandoned
      }

      if (queue.isEmpty() == false) {
        return queue.removeFirst();
      }
      return null;
    }

    public synchronized void appendItemToQueue(final FrostDownloadItem item) {
      final String id = item.getGqIdentifier();
      directGETsInProgress.add(id);

      queue.addLast(item);
      notifyAll(); // notify all waiters (if any) of new record
    }

    public synchronized void appendItemToQueue(final FrostUploadItem item) {
      final String id = item.getGqIdentifier();
      directPUTsInProgress.add(id);

      queue.addLast(item);
      notifyAll(); // notify all waiters (if any) of new record
    }
  }

  public void persistentRequestError(final String id, final NodeMessage nm) {
    if (uploadModelItems.containsKey(id)) {
      final FrostUploadItem item = uploadModelItems.get(id);
      item.setEnabled(false);
      item.setState(FrostUploadItem.STATE_FAILED);
      item.setErrorCodeDescription(nm.getStringValue("CodeDescription"));
    } else if (downloadModelItems.containsKey(id)) {
      final FrostDownloadItem item = downloadModelItems.get(id);
      item.setEnabled(false);
      item.setState(FrostDownloadItem.STATE_FAILED);
      item.setErrorCodeDescription(nm.getStringValue("CodeDescription"));
    } else {
      System.out.println("persistentRequestError: ID not in any model: " + id);
    }
  }

  public void persistentRequestAdded(final FcpPersistentPut uploadRequest) {
    final FrostUploadItem ulItem = uploadModelItems.get(uploadRequest.getIdentifier());
    if (ulItem != null) {
      // own item added to global queue, or existing external item
      applyState(ulItem, uploadRequest);
    } else {
      if (showExternalItemsUpload) {
        addExternalItem(uploadRequest);
      }
    }
  }

  public void persistentRequestAdded(final FcpPersistentGet downloadRequest) {
    final FrostDownloadItem dlItem = downloadModelItems.get(downloadRequest.getIdentifier());
    if (dlItem != null) {
      // own item added to global queue, or existing external item
      applyState(dlItem, downloadRequest);
    } else {
      if (showExternalItemsDownload) {
        addExternalItem(downloadRequest);
      }
    }
  }

  public void persistentRequestModified(final FcpPersistentPut uploadRequest) {
    if (uploadModelItems.containsKey(uploadRequest.getIdentifier())) {
      final FrostUploadItem ulItem = uploadModelItems.get(uploadRequest.getIdentifier());
      ulItem.setPriority(uploadRequest.getPriority());
    }
  }

  public void persistentRequestModified(final FcpPersistentGet downloadRequest) {
    if (downloadModelItems.containsKey(downloadRequest.getIdentifier())) {
      final FrostDownloadItem dlItem = downloadModelItems.get(downloadRequest.getIdentifier());
      applyPriority(dlItem, downloadRequest);
    }
  }

  public void persistentRequestRemoved(final FcpPersistentPut uploadRequest) {
    if (uploadModelItems.containsKey(uploadRequest.getIdentifier())) {
      final FrostUploadItem ulItem = uploadModelItems.get(uploadRequest.getIdentifier());
      if (ulItem.isExternal()) {
        SwingUtilities.invokeLater(
            new Runnable() {
              public void run() {
                List<FrostUploadItem> itemList = new ArrayList<FrostUploadItem>();
                itemList.add(ulItem);
                uploadModel.removeItems(itemList);
              }
            });
      } else {
        if (ulItem.isInternalRemoveExpected()) {
          ulItem.setInternalRemoveExpected(false); // clear flag
        } else if (ulItem.getState() != FrostUploadItem.STATE_DONE) {
          ulItem.setEnabled(false);
          ulItem.setState(FrostUploadItem.STATE_FAILED);
          ulItem.setErrorCodeDescription("Disappeared from global queue");
        }
      }
    }
  }

  public void persistentRequestRemoved(final FcpPersistentGet downloadRequest) {
    if (downloadModelItems.containsKey(downloadRequest.getIdentifier())) {
      final FrostDownloadItem dlItem = downloadModelItems.get(downloadRequest.getIdentifier());
      if (dlItem.isExternal()) {
        SwingUtilities.invokeLater(
            new Runnable() {
              public void run() {
                List<FrostDownloadItem> itemList = new ArrayList<FrostDownloadItem>();
                itemList.add(dlItem);
                downloadModel.removeItems(itemList);
              }
            });
      } else {
        if (dlItem.isInternalRemoveExpected()) {
          dlItem.setInternalRemoveExpected(false); // clear flag
        } else if (dlItem.getState() != FrostDownloadItem.STATE_DONE) {
          dlItem.setEnabled(false);
          dlItem.setState(FrostDownloadItem.STATE_FAILED);
          dlItem.setErrorCodeDescription("Disappeared from global queue");
        }
      }
    }
  }

  public void persistentRequestUpdated(final FcpPersistentPut uploadRequest) {
    final FrostUploadItem ui = uploadModelItems.get(uploadRequest.getIdentifier());
    if (ui == null) {
      // not (yet) in our model
      return;
    }
    applyState(ui, uploadRequest);
  }

  public void persistentRequestUpdated(final FcpPersistentGet downloadRequest) {
    final FrostDownloadItem dl = downloadModelItems.get(downloadRequest.getIdentifier());
    if (dl == null) {
      // not (yet) in our model
      return;
    }
    applyState(dl, downloadRequest);
  }
}
Exemple #6
0
/**
 * The <tt>StatusSubMenu</tt> provides a menu which allow to select the status for each of the
 * protocol providers registered when the menu appears
 *
 * @author Nicolas Chamouard
 */
public class StatusSubMenu extends JMenu {
  /** A reference of <tt>Systray</tt> */
  private SystrayServiceJdicImpl parentSystray;

  /** Contains all accounts and corresponding menus. */
  private Hashtable accountSelectors = new Hashtable();

  private Logger logger = Logger.getLogger(StatusSubMenu.class);

  /**
   * Creates an instance of <tt>StatusSubMenu</tt>.
   *
   * @param tray a reference of the parent <tt>Systray</tt>
   */
  public StatusSubMenu(SystrayServiceJdicImpl tray) {

    parentSystray = tray;

    this.setText(Resources.getString("setStatus"));
    this.setIcon(Resources.getImage("statusMenuIcon"));

    /* makes the menu look better */
    this.setPreferredSize(new java.awt.Dimension(28, 24));

    this.init();
  }

  /**
   * Adds the account corresponding to the given protocol provider to this menu.
   *
   * @param protocolProvider the protocol provider corresponding to the account to add
   */
  private void addAccount(ProtocolProviderService protocolProvider) {
    OperationSetPresence presence =
        (OperationSetPresence) protocolProvider.getOperationSet(OperationSetPresence.class);

    if (presence == null) {
      StatusSimpleSelector simpleSelector =
          new StatusSimpleSelector(parentSystray, protocolProvider);

      this.accountSelectors.put(protocolProvider.getAccountID(), simpleSelector);
      this.add(simpleSelector);
    } else {
      StatusSelector statusSelector = new StatusSelector(parentSystray, protocolProvider, presence);

      this.accountSelectors.put(protocolProvider.getAccountID(), statusSelector);
      this.add(statusSelector);

      presence.addProviderPresenceStatusListener(new SystrayProviderPresenceStatusListener());
    }
  }

  /**
   * Removes the account corresponding to the given protocol provider from this menu.
   *
   * @param protocolProvider the protocol provider corresponding to the account to remove.
   */
  private void removeAccount(ProtocolProviderService protocolProvider) {
    Component c = (Component) this.accountSelectors.get(protocolProvider.getAccountID());

    this.remove(c);
  }

  /**
   * We fill the protocolProviderTable with all running protocol providers at the start of the
   * bundle.
   */
  private void init() {
    SystrayActivator.bundleContext.addServiceListener(new ProtocolProviderServiceListener());

    ServiceReference[] protocolProviderRefs = null;
    try {
      protocolProviderRefs =
          SystrayActivator.bundleContext.getServiceReferences(
              ProtocolProviderService.class.getName(), null);
    } catch (InvalidSyntaxException ex) {
      // this shouldn't happen since we're providing no parameter string
      // but let's log just in case.
      logger.error("Error while retrieving service refs", ex);
      return;
    }

    // in case we found any
    if (protocolProviderRefs != null) {

      for (int i = 0; i < protocolProviderRefs.length; i++) {
        ProtocolProviderService provider =
            (ProtocolProviderService)
                SystrayActivator.bundleContext.getService(protocolProviderRefs[i]);

        boolean isHidden =
            provider.getAccountID().getAccountProperties().get("HIDDEN_PROTOCOL") != null;

        if (!isHidden) this.addAccount(provider);
      }
    }
  }

  /**
   * Listens for <tt>ServiceEvent</tt>s indicating that a <tt>ProtocolProviderService</tt> has been
   * registered and completes the account status menu.
   */
  private class ProtocolProviderServiceListener implements ServiceListener {
    /**
     * When a service is registered or unregistered, we update the provider tables and add/remove
     * listeners (if it supports BasicInstantMessenging implementation)
     *
     * @param event ServiceEvent
     */
    public void serviceChanged(ServiceEvent event) {
      // if the event is caused by a bundle being stopped, we don't want to
      // know
      if (event.getServiceReference().getBundle().getState() == Bundle.STOPPING) {
        return;
      }

      Object service = SystrayActivator.bundleContext.getService(event.getServiceReference());

      if (!(service instanceof ProtocolProviderService)) return;

      ProtocolProviderService provider = (ProtocolProviderService) service;

      if (event.getType() == ServiceEvent.REGISTERED) addAccount(provider);

      if (event.getType() == ServiceEvent.UNREGISTERING) removeAccount(provider);
    }
  }

  /**
   * Listens for all providerStatusChanged and providerStatusMessageChanged events in order to
   * refresh the account status panel, when a status is changed.
   */
  private class SystrayProviderPresenceStatusListener implements ProviderPresenceStatusListener {
    /** Fired when an account has changed its status. We update the icon in the menu. */
    public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
      ProtocolProviderService pps = evt.getProvider();

      StatusSelector selectorBox = (StatusSelector) accountSelectors.get(pps.getAccountID());

      if (selectorBox == null) return;

      selectorBox.updateStatus(evt.getNewStatus());
    }

    public void providerStatusMessageChanged(PropertyChangeEvent evt) {}
  }
}
/**
 * The source contact service. The will show most recent messages.
 *
 * @author Damian Minkov
 */
public class MessageSourceService extends MetaContactListAdapter
    implements ContactSourceService,
        ContactPresenceStatusListener,
        ContactCapabilitiesListener,
        ProviderPresenceStatusListener,
        SubscriptionListener,
        LocalUserChatRoomPresenceListener,
        MessageListener,
        ChatRoomMessageListener,
        AdHocChatRoomMessageListener {
  /** The logger for this class. */
  private static Logger logger = Logger.getLogger(MessageSourceService.class);

  /** The display name of this contact source. */
  private final String MESSAGE_HISTORY_NAME;

  /** The type of the source service, the place to be shown in the ui. */
  private int sourceServiceType = CONTACT_LIST_TYPE;

  /**
   * Whether to show recent messages in history or in contactlist. By default we show it in
   * contactlist.
   */
  private static final String IN_HISTORY_PROPERTY =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IN_HISTORY";

  /** Property to control number of recent messages. */
  private static final String NUMBER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_NUMBER";

  /** Property to control version of recent messages. */
  private static final String VER_OF_RECENT_MSGS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.MSG_VER";

  /** Property to control messages type. Can query for message sub type. */
  private static final String IS_MESSAGE_SUBTYPE_SMS_PROP =
      "net.java.sip.communicator.impl.msghistory.contactsrc.IS_SMS_ENABLED";

  /**
   * The number of recent messages to store in the history, but will retrieve just
   * <tt>numberOfMessages</tt>
   */
  private static final int NUMBER_OF_MSGS_IN_HISTORY = 100;

  /** Number of messages to show. */
  private int numberOfMessages = 10;

  /** The structure to save recent messages list. */
  private static final String[] STRUCTURE_NAMES =
      new String[] {"provider", "contact", "timestamp", "ver"};

  /** The current version of recent messages. When changed the recent messages are recreated. */
  private static String RECENT_MSGS_VER = "2";

  /** The structure. */
  private static final HistoryRecordStructure recordStructure =
      new HistoryRecordStructure(STRUCTURE_NAMES);

  /** Recent messages history ID. */
  private static final HistoryID historyID =
      HistoryID.createFromRawID(new String[] {"recent_messages"});

  /** The cache for recent messages. */
  private History history = null;

  /** List of recent messages. */
  private final List<ComparableEvtObj> recentMessages = new LinkedList<ComparableEvtObj>();

  /** Date of the oldest shown message. */
  private Date oldestRecentMessage = null;

  /** The last query created. */
  private MessageSourceContactQuery recentQuery = null;

  /** The message subtype if any. */
  private boolean isSMSEnabled = false;

  /** Message history service that has created us. */
  private MessageHistoryServiceImpl messageHistoryService;

  /** Constructs MessageSourceService. */
  MessageSourceService(MessageHistoryServiceImpl messageHistoryService) {
    this.messageHistoryService = messageHistoryService;

    ConfigurationService conf = MessageHistoryActivator.getConfigurationService();

    if (conf.getBoolean(IN_HISTORY_PROPERTY, false)) {
      sourceServiceType = HISTORY_TYPE;
    }

    MESSAGE_HISTORY_NAME =
        MessageHistoryActivator.getResources().getI18NString("service.gui.RECENT_MESSAGES");

    numberOfMessages = conf.getInt(NUMBER_OF_RECENT_MSGS_PROP, numberOfMessages);

    isSMSEnabled = conf.getBoolean(IS_MESSAGE_SUBTYPE_SMS_PROP, isSMSEnabled);

    RECENT_MSGS_VER = conf.getString(VER_OF_RECENT_MSGS_PROP, RECENT_MSGS_VER);

    MessageSourceContactPresenceStatus.MSG_SRC_CONTACT_ONLINE.setStatusIcon(
        MessageHistoryActivator.getResources()
            .getImageInBytes("service.gui.icons.SMS_STATUS_ICON"));
  }

  /**
   * Returns the display name of this contact source.
   *
   * @return the display name of this contact source
   */
  @Override
  public String getDisplayName() {
    return MESSAGE_HISTORY_NAME;
  }

  /**
   * Returns default type to indicate that this contact source can be queried by default filters.
   *
   * @return the type of this contact source
   */
  @Override
  public int getType() {
    return sourceServiceType;
  }

  /**
   * Returns the index of the contact source in the result list.
   *
   * @return the index of the contact source in the result list
   */
  @Override
  public int getIndex() {
    return 0;
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString) {
    recentQuery = (MessageSourceContactQuery) createContactQuery(queryString, numberOfMessages);

    return recentQuery;
  }

  /**
   * Updates the contact sources in the recent query if any. Done here in order to sync with
   * recentMessages instance, and to check for already existing instances of contact sources.
   * Normally called from the query.
   */
  public void updateRecentMessages() {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      List<SourceContact> currentContactsInQuery = recentQuery.getQueryResults();

      for (ComparableEvtObj evtObj : recentMessages) {
        // the contains will use the correct equals method of
        // the object evtObj
        if (!currentContactsInQuery.contains(evtObj)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(evtObj.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(evtObj.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }
    }
  }

  /**
   * Searches for entries in cached recent messages in history.
   *
   * @param provider the provider which contact messages we will search
   * @param isStatusChanged is the search because of status changed
   * @return entries in cached recent messages in history.
   */
  private List<ComparableEvtObj> getCachedRecentMessages(
      ProtocolProviderService provider, boolean isStatusChanged) {
    String providerID = provider.getAccountID().getAccountUniqueID();
    List<String> recentMessagesContactIDs =
        getRecentContactIDs(
            providerID, recentMessages.size() < numberOfMessages ? null : oldestRecentMessage);

    List<ComparableEvtObj> cachedRecentMessages = new ArrayList<ComparableEvtObj>();

    for (String contactID : recentMessagesContactIDs) {
      Collection<EventObject> res =
          messageHistoryService.findRecentMessagesPerContact(
              numberOfMessages, providerID, contactID, isSMSEnabled);

      processEventObjects(res, cachedRecentMessages, isStatusChanged);
    }

    return cachedRecentMessages;
  }

  /**
   * Process list of event objects. Checks whether message source contact already exist for this
   * event object, if yes just update it with the new values (not sure whether we should do this, as
   * it may bring old messages) and if status of provider is changed, init its details, updates its
   * capabilities. It still adds the found messages source contact to the list of the new contacts,
   * as later we will detect this and fire update event. If nothing found a new contact is created.
   *
   * @param res list of event
   * @param cachedRecentMessages list of newly created source contacts or already existed but
   *     updated with corresponding event object
   * @param isStatusChanged whether provider status changed and we are processing
   */
  private void processEventObjects(
      Collection<EventObject> res,
      List<ComparableEvtObj> cachedRecentMessages,
      boolean isStatusChanged) {
    for (EventObject obj : res) {
      ComparableEvtObj oldMsg = findRecentMessage(obj, recentMessages);

      if (oldMsg != null) {
        oldMsg.update(obj); // update

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        // we still add it to cachedRecentMessages
        // later we will find it is duplicate and will fire
        // update event
        if (!cachedRecentMessages.contains(oldMsg)) cachedRecentMessages.add(oldMsg);

        continue;
      }

      oldMsg = findRecentMessage(obj, cachedRecentMessages);

      if (oldMsg == null) {
        oldMsg = new ComparableEvtObj(obj);

        if (isStatusChanged && recentQuery != null) recentQuery.updateCapabilities(oldMsg, obj);

        cachedRecentMessages.add(oldMsg);
      }
    }
  }

  /**
   * Access for source contacts impl.
   *
   * @return
   */
  boolean isSMSEnabled() {
    return isSMSEnabled;
  }

  /**
   * Add the ComparableEvtObj, newly added will fire new, for existing fire update and when trimming
   * the list to desired length fire remove for those that were removed
   *
   * @param contactsToAdd
   */
  private void addNewRecentMessages(List<ComparableEvtObj> contactsToAdd) {
    // now find object to fire new, and object to fire remove
    // let us find duplicates and fire update
    List<ComparableEvtObj> duplicates = new ArrayList<ComparableEvtObj>();
    for (ComparableEvtObj msgToAdd : contactsToAdd) {
      if (recentMessages.contains(msgToAdd)) {
        duplicates.add(msgToAdd);

        // save update
        updateRecentMessageToHistory(msgToAdd);
      }
    }
    recentMessages.removeAll(duplicates);

    // now contacts to add has no duplicates, add them all
    boolean changed = recentMessages.addAll(contactsToAdd);

    if (changed) {
      Collections.sort(recentMessages);

      if (recentQuery != null) {
        for (ComparableEvtObj obj : duplicates)
          recentQuery.updateContact(obj, obj.getEventObject());
      }
    }

    if (!recentMessages.isEmpty())
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

    // trim
    List<ComparableEvtObj> removedItems = null;
    if (recentMessages.size() > numberOfMessages) {
      removedItems =
          new ArrayList<ComparableEvtObj>(
              recentMessages.subList(numberOfMessages, recentMessages.size()));

      recentMessages.removeAll(removedItems);
    }

    if (recentQuery != null) {
      // now fire, removed for all that were in the list
      // and now are removed after trim
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          if (!contactsToAdd.contains(msc)) recentQuery.fireContactRemoved(msc);
        }
      }

      // fire new for all that were added, and not removed after trim
      for (ComparableEvtObj msc : contactsToAdd) {
        if ((removedItems == null || !removedItems.contains(msc)) && !duplicates.contains(msc)) {
          MessageSourceContact newSourceContact =
              new MessageSourceContact(msc.getEventObject(), MessageSourceService.this);
          newSourceContact.initDetails(msc.getEventObject());

          recentQuery.addQueryResult(newSourceContact);
        }
      }

      // if recent messages were changed, indexes have change lets
      // fire event for the last element which will reorder the whole
      // group if needed.
      if (changed) recentQuery.fireContactChanged(recentMessages.get(recentMessages.size() - 1));
    }
  }

  /**
   * When a provider is added, do not block and start executing in new thread.
   *
   * @param provider ProtocolProviderService
   */
  void handleProviderAdded(final ProtocolProviderService provider, final boolean isStatusChanged) {
    new Thread(
            new Runnable() {
              @Override
              public void run() {
                handleProviderAddedInSeparateThread(provider, isStatusChanged);
              }
            })
        .start();
  }

  /**
   * When a provider is added. As searching can be slow especially when handling special type of
   * messages (with subType) this need to be run in new Thread.
   *
   * @param provider ProtocolProviderService
   */
  private void handleProviderAddedInSeparateThread(
      ProtocolProviderService provider, boolean isStatusChanged) {
    // lets check if we have cached recent messages for this provider, and
    // fire events if found and are newer

    synchronized (recentMessages) {
      List<ComparableEvtObj> cachedRecentMessages =
          getCachedRecentMessages(provider, isStatusChanged);

      if (cachedRecentMessages.isEmpty()) {
        // maybe there is no cached history for this
        // let's check
        // load it not from cache, but do a local search
        Collection<EventObject> res =
            messageHistoryService.findRecentMessagesPerContact(
                numberOfMessages, provider.getAccountID().getAccountUniqueID(), null, isSMSEnabled);

        List<ComparableEvtObj> newMsc = new ArrayList<ComparableEvtObj>();

        processEventObjects(res, newMsc, isStatusChanged);

        addNewRecentMessages(newMsc);

        for (ComparableEvtObj msc : newMsc) {
          saveRecentMessageToHistory(msc);
        }
      } else addNewRecentMessages(cachedRecentMessages);
    }
  }

  /**
   * Tries to match the event object to already existing ComparableEvtObj in the supplied list.
   *
   * @param obj the object that we will try to match.
   * @param list the list we will search in.
   * @return the found ComparableEvtObj
   */
  private static ComparableEvtObj findRecentMessage(EventObject obj, List<ComparableEvtObj> list) {
    Contact contact = null;
    ChatRoom chatRoom = null;

    if (obj instanceof MessageDeliveredEvent) {
      contact = ((MessageDeliveredEvent) obj).getDestinationContact();
    } else if (obj instanceof MessageReceivedEvent) {
      contact = ((MessageReceivedEvent) obj).getSourceContact();
    } else if (obj instanceof ChatRoomMessageDeliveredEvent) {
      chatRoom = ((ChatRoomMessageDeliveredEvent) obj).getSourceChatRoom();
    } else if (obj instanceof ChatRoomMessageReceivedEvent) {
      chatRoom = ((ChatRoomMessageReceivedEvent) obj).getSourceChatRoom();
    }

    for (ComparableEvtObj evt : list) {
      if ((contact != null && contact.equals(evt.getContact()))
          || (chatRoom != null && chatRoom.equals(evt.getRoom()))) return evt;
    }

    return null;
  }

  /**
   * A provider has been removed.
   *
   * @param provider the ProtocolProviderService that has been unregistered.
   */
  void handleProviderRemoved(ProtocolProviderService provider) {
    // lets remove the recent messages for this provider, and update
    // with recent messages for the available providers
    synchronized (recentMessages) {
      if (provider != null) {
        List<ComparableEvtObj> removedItems = new ArrayList<ComparableEvtObj>();
        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)) removedItems.add(msc);
        }

        recentMessages.removeAll(removedItems);
        if (!recentMessages.isEmpty())
          oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();
        else oldestRecentMessage = null;

        if (recentQuery != null) {
          for (ComparableEvtObj msc : removedItems) {
            recentQuery.fireContactRemoved(msc);
          }
        }
      }

      // handleProviderRemoved can be invoked due to stopped
      // history service, if this is the case we do not want to
      // update messages
      if (!this.messageHistoryService.isHistoryLoggingEnabled()) return;

      // lets do the same as we enable provider
      // for all registered providers and finally fire events
      List<ComparableEvtObj> contactsToAdd = new ArrayList<ComparableEvtObj>();
      for (ProtocolProviderService pps : messageHistoryService.getCurrentlyAvailableProviders()) {
        contactsToAdd.addAll(getCachedRecentMessages(pps, true));
      }

      addNewRecentMessages(contactsToAdd);
    }
  }

  /**
   * Searches for contact ids in history of recent messages.
   *
   * @param provider
   * @param after
   * @return
   */
  List<String> getRecentContactIDs(String provider, Date after) {
    List<String> res = new ArrayList<String>();

    try {
      History history = getHistory();

      if (history != null) {
        Iterator<HistoryRecord> recs = history.getReader().findLast(NUMBER_OF_MSGS_IN_HISTORY);
        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        while (recs.hasNext()) {
          HistoryRecord hr = recs.next();

          String contact = null;
          String recordProvider = null;
          Date timestamp = null;

          for (int i = 0; i < hr.getPropertyNames().length; i++) {
            String propName = hr.getPropertyNames()[i];

            if (propName.equals(STRUCTURE_NAMES[0])) recordProvider = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[1])) contact = hr.getPropertyValues()[i];
            else if (propName.equals(STRUCTURE_NAMES[2])) {
              try {
                timestamp = sdf.parse(hr.getPropertyValues()[i]);
              } catch (ParseException e) {
                timestamp = new Date(Long.parseLong(hr.getPropertyValues()[i]));
              }
            }
          }

          if (recordProvider == null || contact == null) continue;

          if (after != null && timestamp != null && timestamp.before(after)) continue;

          if (recordProvider.equals(provider)) res.add(contact);
        }
      }
    } catch (IOException ex) {
      logger.error("cannot create recent_messages history", ex);
    }

    return res;
  }

  /**
   * Returns the cached recent messages history.
   *
   * @return
   * @throws IOException
   */
  private History getHistory() throws IOException {
    synchronized (historyID) {
      HistoryService historyService =
          MessageHistoryActivator.getMessageHistoryService().getHistoryService();

      if (history == null) {
        history = historyService.createHistory(historyID, recordStructure);

        // lets check the version if not our version, re-create
        // history (delete it)
        HistoryReader reader = history.getReader();
        boolean delete = false;
        QueryResultSet<HistoryRecord> res = reader.findLast(1);
        if (res != null && res.hasNext()) {
          HistoryRecord hr = res.next();
          if (hr.getPropertyValues().length >= 4) {
            if (!hr.getPropertyValues()[3].equals(RECENT_MSGS_VER)) delete = true;
          } else delete = true;
        }

        if (delete) {
          // delete it
          try {
            historyService.purgeLocallyStoredHistory(historyID);

            history = historyService.createHistory(historyID, recordStructure);
          } catch (IOException ex) {
            logger.error("Cannot delete recent_messages history", ex);
          }
        }
      }

      return history;
    }
  }

  /**
   * Returns the index of the source contact, in the list of recent messages.
   *
   * @param messageSourceContact
   * @return
   */
  int getIndex(MessageSourceContact messageSourceContact) {
    synchronized (recentMessages) {
      for (int i = 0; i < recentMessages.size(); i++)
        if (recentMessages.get(i).equals(messageSourceContact)) return i;

      return -1;
    }
  }

  /**
   * Creates query for the given <tt>searchString</tt>.
   *
   * @param queryString the string to search for
   * @param contactCount the maximum count of result contacts
   * @return the created query
   */
  @Override
  public ContactQuery createContactQuery(String queryString, int contactCount) {
    if (!StringUtils.isNullOrEmpty(queryString)) return null;

    recentQuery = new MessageSourceContactQuery(MessageSourceService.this);

    return recentQuery;
  }

  /**
   * Updates contact source contacts with status.
   *
   * @param evt the ContactPresenceStatusChangeEvent describing the status
   */
  @Override
  public void contactPresenceStatusChanged(ContactPresenceStatusChangeEvent evt) {
    if (recentQuery == null) return;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getContact() != null && msg.getContact().equals(evt.getSourceContact())) {
          recentQuery.updateContactStatus(msg, evt.getNewStatus());
        }
      }
    }
  }

  @Override
  public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
    if (!evt.getNewStatus().isOnline() || evt.getOldStatus().isOnline()) return;

    handleProviderAdded(evt.getProvider(), true);
  }

  @Override
  public void providerStatusMessageChanged(PropertyChangeEvent evt) {}

  @Override
  public void localUserPresenceChanged(LocalUserChatRoomPresenceChangeEvent evt) {
    if (recentQuery == null) return;

    ComparableEvtObj srcContact = null;

    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(evt.getChatRoom())) {
          srcContact = msg;
          break;
        }
      }
    }

    if (srcContact == null) return;

    String eventType = evt.getEventType();

    if (LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED.equals(eventType)) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_ONLINE);
    } else if ((LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_LEFT.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_KICKED.equals(eventType)
        || LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_DROPPED.equals(eventType))) {
      recentQuery.updateContactStatus(srcContact, ChatRoomPresenceStatus.CHAT_ROOM_OFFLINE);
    }
  }

  /**
   * Handles new events.
   *
   * @param obj the event object
   * @param provider the provider
   * @param id the id of the source of the event
   */
  private void handle(EventObject obj, ProtocolProviderService provider, String id) {
    // check if provider - contact exist update message content
    synchronized (recentMessages) {
      ComparableEvtObj existingMsc = null;
      for (ComparableEvtObj msc : recentMessages) {
        if (msc.getProtocolProviderService().equals(provider)
            && msc.getContactAddress().equals(id)) {
          // update
          msc.update(obj);
          updateRecentMessageToHistory(msc);

          existingMsc = msc;
        }
      }

      if (existingMsc != null) {
        Collections.sort(recentMessages);
        oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

        if (recentQuery != null) {
          recentQuery.updateContact(existingMsc, existingMsc.getEventObject());
          recentQuery.fireContactChanged(existingMsc);
        }

        return;
      }

      // if missing create source contact
      // and update recent messages, trim and sort
      MessageSourceContact newSourceContact =
          new MessageSourceContact(obj, MessageSourceService.this);
      newSourceContact.initDetails(obj);
      // we have already checked for duplicate
      ComparableEvtObj newMsg = new ComparableEvtObj(obj);
      recentMessages.add(newMsg);

      Collections.sort(recentMessages);
      oldestRecentMessage = recentMessages.get(recentMessages.size() - 1).getTimestamp();

      // trim
      List<ComparableEvtObj> removedItems = null;
      if (recentMessages.size() > numberOfMessages) {
        removedItems =
            new ArrayList<ComparableEvtObj>(
                recentMessages.subList(numberOfMessages, recentMessages.size()));

        recentMessages.removeAll(removedItems);
      }

      // save
      saveRecentMessageToHistory(newMsg);

      // no query nothing to fire
      if (recentQuery == null) return;

      // now fire
      if (removedItems != null) {
        for (ComparableEvtObj msc : removedItems) {
          recentQuery.fireContactRemoved(msc);
        }
      }

      recentQuery.addQueryResult(newSourceContact);
    }
  }

  /** Adds recent message in history. */
  private void saveRecentMessageToHistory(ComparableEvtObj msc) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();
        HistoryWriter writer = history.getWriter();

        SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

        writer.addRecord(
            new String[] {
              msc.getProtocolProviderService().getAccountID().getAccountUniqueID(),
              msc.getContactAddress(),
              sdf.format(msc.getTimestamp()),
              RECENT_MSGS_VER
            },
            NUMBER_OF_MSGS_IN_HISTORY);
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  /** Updates recent message in history. */
  private void updateRecentMessageToHistory(final ComparableEvtObj msg) {
    synchronized (historyID) {
      // and create it
      try {
        History history = getHistory();

        HistoryWriter writer = history.getWriter();

        writer.updateRecord(
            new HistoryWriter.HistoryRecordUpdater() {
              HistoryRecord hr;

              @Override
              public void setHistoryRecord(HistoryRecord historyRecord) {
                this.hr = historyRecord;
              }

              @Override
              public boolean isMatching() {
                boolean providerFound = false;
                boolean contactFound = false;
                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    if (msg.getProtocolProviderService()
                        .getAccountID()
                        .getAccountUniqueID()
                        .equals(hr.getPropertyValues()[i])) {
                      providerFound = true;
                    }
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    if (msg.getContactAddress().equals(hr.getPropertyValues()[i])) {
                      contactFound = true;
                    }
                  }
                }

                return contactFound && providerFound;
              }

              @Override
              public Map<String, String> getUpdateChanges() {
                HashMap<String, String> map = new HashMap<String, String>();
                SimpleDateFormat sdf = new SimpleDateFormat(HistoryService.DATE_FORMAT);

                for (int i = 0; i < hr.getPropertyNames().length; i++) {
                  String propName = hr.getPropertyNames()[i];

                  if (propName.equals(STRUCTURE_NAMES[0])) {
                    map.put(
                        propName,
                        msg.getProtocolProviderService().getAccountID().getAccountUniqueID());
                  } else if (propName.equals(STRUCTURE_NAMES[1])) {
                    map.put(propName, msg.getContactAddress());
                  } else if (propName.equals(STRUCTURE_NAMES[2])) {
                    map.put(propName, sdf.format(msg.getTimestamp()));
                  } else if (propName.equals(STRUCTURE_NAMES[3]))
                    map.put(propName, RECENT_MSGS_VER);
                }

                return map;
              }
            });
      } catch (IOException ex) {
        logger.error("cannot create recent_messages history", ex);
        return;
      }
    }
  }

  @Override
  public void messageReceived(MessageReceivedEvent evt) {
    if (isSMSEnabled && evt.getEventType() != MessageReceivedEvent.SMS_MESSAGE_RECEIVED) {
      return;
    }

    handle(evt, evt.getSourceContact().getProtocolProvider(), evt.getSourceContact().getAddress());
  }

  @Override
  public void messageDelivered(MessageDeliveredEvent evt) {
    if (isSMSEnabled && !evt.isSmsMessage()) return;

    handle(
        evt,
        evt.getDestinationContact().getProtocolProvider(),
        evt.getDestinationContact().getAddress());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>MessageFailedEvent</tt> containing the ID of the
   */
  @Override
  public void messageDeliveryFailed(MessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(ChatRoomMessageReceivedEvent evt) {
    if (isSMSEnabled) return;

    // ignore non conversation messages
    if (evt.getEventType() != ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  @Override
  public void messageDelivered(ChatRoomMessageDeliveredEvent evt) {
    if (isSMSEnabled) return;

    handle(
        evt, evt.getSourceChatRoom().getParentProvider(), evt.getSourceChatRoom().getIdentifier());
  }

  /**
   * Not used.
   *
   * @param evt the <tt>ChatroomMessageDeliveryFailedEvent</tt> containing
   */
  @Override
  public void messageDeliveryFailed(ChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void messageReceived(AdHocChatRoomMessageReceivedEvent evt) {
    // TODO
  }

  @Override
  public void messageDelivered(AdHocChatRoomMessageDeliveredEvent evt) {
    // TODO
  }

  /**
   * Not used.
   *
   * @param evt the <tt>AdHocChatroomMessageDeliveryFailedEvent</tt>
   */
  @Override
  public void messageDeliveryFailed(AdHocChatRoomMessageDeliveryFailedEvent evt) {}

  @Override
  public void subscriptionCreated(SubscriptionEvent evt) {}

  @Override
  public void subscriptionFailed(SubscriptionEvent evt) {}

  @Override
  public void subscriptionRemoved(SubscriptionEvent evt) {}

  @Override
  public void subscriptionMoved(SubscriptionMovedEvent evt) {}

  @Override
  public void subscriptionResolved(SubscriptionEvent evt) {}

  /**
   * If a contact is renamed update the locally stored message if any.
   *
   * @param evt the <tt>ContactPropertyChangeEvent</tt> containing the source
   */
  @Override
  public void contactModified(ContactPropertyChangeEvent evt) {
    if (!evt.getPropertyName().equals(ContactPropertyChangeEvent.PROPERTY_DISPLAY_NAME)) return;

    Contact contact = evt.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null)
          recentQuery.updateContactDisplayName(msc, contact.getDisplayName());

        return;
      }
    }
  }

  /**
   * Indicates that a MetaContact has been modified.
   *
   * @param evt the MetaContactListEvent containing the corresponding contact
   */
  public void metaContactRenamed(MetaContactRenamedEvent evt) {
    for (ComparableEvtObj msc : recentMessages) {
      if (evt.getSourceMetaContact().containsContact(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateContactDisplayName(msc, evt.getNewDisplayName());
      }
    }
  }

  @Override
  public void supportedOperationSetsChanged(ContactCapabilitiesEvent event) {
    Contact contact = event.getSourceContact();

    if (contact == null) return;

    for (ComparableEvtObj msc : recentMessages) {
      if (contact.equals(msc.getContact())) {
        if (recentQuery != null) recentQuery.updateCapabilities(msc, contact);

        return;
      }
    }
  }

  /** Permanently removes all locally stored message history, remove recent contacts. */
  public void eraseLocallyStoredHistory() throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>(recentMessages);

      recentMessages.clear();
    }

    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the metacontact, remove any recent
   * contacts if any.
   */
  public void eraseLocallyStoredHistory(MetaContact contact) throws IOException {
    List<ComparableEvtObj> toRemove = null;
    synchronized (recentMessages) {
      toRemove = new ArrayList<ComparableEvtObj>();
      Iterator<Contact> iter = contact.getContacts();
      while (iter.hasNext()) {
        Contact item = iter.next();
        String id = item.getAddress();
        ProtocolProviderService provider = item.getProtocolProvider();

        for (ComparableEvtObj msc : recentMessages) {
          if (msc.getProtocolProviderService().equals(provider)
              && msc.getContactAddress().equals(id)) {
            toRemove.add(msc);
          }
        }
      }

      recentMessages.removeAll(toRemove);
    }
    if (recentQuery != null) {
      for (ComparableEvtObj msc : toRemove) {
        recentQuery.fireContactRemoved(msc);
      }
    }
  }

  /**
   * Permanently removes locally stored message history for the chatroom, remove any recent contacts
   * if any.
   */
  public void eraseLocallyStoredHistory(ChatRoom room) {
    ComparableEvtObj toRemove = null;
    synchronized (recentMessages) {
      for (ComparableEvtObj msg : recentMessages) {
        if (msg.getRoom() != null && msg.getRoom().equals(room)) {
          toRemove = msg;
          break;
        }
      }

      if (toRemove == null) return;

      recentMessages.remove(toRemove);
    }

    if (recentQuery != null) recentQuery.fireContactRemoved(toRemove);
  }

  /** Object used to cache recent messages. */
  private class ComparableEvtObj implements Comparable<ComparableEvtObj> {
    private EventObject eventObject;

    /** The protocol provider. */
    private ProtocolProviderService ppService = null;

    /** The address. */
    private String address = null;

    /** The timestamp. */
    private Date timestamp = null;

    /** The contact instance. */
    private Contact contact = null;

    /** The room instance. */
    private ChatRoom room = null;

    /**
     * Constructs.
     *
     * @param source used to extract initial values.
     */
    ComparableEvtObj(EventObject source) {
      update(source);
    }

    /**
     * Extract values from <tt>EventObject</tt>.
     *
     * @param source
     */
    public void update(EventObject source) {
      this.eventObject = source;

      if (source instanceof MessageDeliveredEvent) {
        MessageDeliveredEvent e = (MessageDeliveredEvent) source;

        this.contact = e.getDestinationContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof MessageReceivedEvent) {
        MessageReceivedEvent e = (MessageReceivedEvent) source;

        this.contact = e.getSourceContact();

        this.address = contact.getAddress();
        this.ppService = contact.getProtocolProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageDeliveredEvent) {
        ChatRoomMessageDeliveredEvent e = (ChatRoomMessageDeliveredEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      } else if (source instanceof ChatRoomMessageReceivedEvent) {
        ChatRoomMessageReceivedEvent e = (ChatRoomMessageReceivedEvent) source;

        this.room = e.getSourceChatRoom();

        this.address = room.getIdentifier();
        this.ppService = room.getParentProvider();
        this.timestamp = e.getTimestamp();
      }
    }

    @Override
    public String toString() {
      return "ComparableEvtObj{" + "address='" + address + '\'' + ", ppService=" + ppService + '}';
    }

    /**
     * The timestamp of the message.
     *
     * @return the timestamp of the message.
     */
    public Date getTimestamp() {
      return timestamp;
    }

    /**
     * The contact.
     *
     * @return the contact.
     */
    public Contact getContact() {
      return contact;
    }

    /**
     * The room.
     *
     * @return the room.
     */
    public ChatRoom getRoom() {
      return room;
    }

    /**
     * The protocol provider.
     *
     * @return the protocol provider.
     */
    public ProtocolProviderService getProtocolProviderService() {
      return ppService;
    }

    /**
     * The address.
     *
     * @return the address.
     */
    public String getContactAddress() {
      if (this.address != null) return this.address;

      return null;
    }

    /**
     * The event object.
     *
     * @return the event object.
     */
    public EventObject getEventObject() {
      return eventObject;
    }

    /**
     * Compares two ComparableEvtObj.
     *
     * @param o the object to compare with
     * @return 0, less than zero, greater than zero, if equals, less or greater.
     */
    @Override
    public int compareTo(ComparableEvtObj o) {
      if (o == null || o.getTimestamp() == null) return 1;

      return o.getTimestamp().compareTo(getTimestamp());
    }

    /**
     * Checks if equals, and if this event object is used to create a MessageSourceContact, if the
     * supplied <tt>Object</tt> is instance of MessageSourceContact.
     *
     * @param o the object to check.
     * @return <tt>true</tt> if equals.
     */
    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || (!(o instanceof MessageSourceContact) && getClass() != o.getClass()))
        return false;

      if (o instanceof ComparableEvtObj) {
        ComparableEvtObj that = (ComparableEvtObj) o;

        if (!address.equals(that.address)) return false;
        if (!ppService.equals(that.ppService)) return false;
      } else if (o instanceof MessageSourceContact) {
        MessageSourceContact that = (MessageSourceContact) o;

        if (!address.equals(that.getContactAddress())) return false;
        if (!ppService.equals(that.getProtocolProviderService())) return false;
      } else return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = address.hashCode();
      result = 31 * result + ppService.hashCode();
      return result;
    }
  }
}
/**
 * The <tt>AndroidLoginRenderer</tt> is the Android renderer for login events.
 *
 * @author Yana Stamcheva
 * @author Pawel Domas
 */
public class AndroidLoginRenderer implements LoginRenderer {
  /** The logger */
  private static final Logger logger = Logger.getLogger(AndroidLoginRenderer.class);

  /** The <tt>CallListener</tt>. */
  private CallListener androidCallListener;

  /** The android implementation of the provider presence listener. */
  private final ProviderPresenceStatusListener androidPresenceListener =
      new UIProviderPresenceStatusListener();

  /** The security authority used by this login renderer. */
  private final SecurityAuthority securityAuthority;

  /** Authorization handler instance. */
  private final AuthorizationHandlerImpl authorizationHandler;

  /** Cached global status value */
  private PresenceStatus globalStatus;

  /** List of global status listeners. */
  private EventListenerList<PresenceStatus> globalStatusListeners =
      new EventListenerList<PresenceStatus>();

  /** Caches avatar image to track the changes */
  private byte[] localAvatarRaw;

  /** Local avatar drawable */
  private Drawable localAvatar;

  /** Caches local status to track the changes */
  private byte[] localStatusRaw;

  /** Local status drawable */
  private Drawable localStatusDrawable;

  /**
   * Creates an instance of <tt>AndroidLoginRenderer</tt> by specifying the current
   * <tt>Context</tt>.
   *
   * @param defaultSecurityAuthority the security authority that will be used by this login renderer
   */
  public AndroidLoginRenderer(SecurityAuthority defaultSecurityAuthority) {
    androidCallListener = new AndroidCallListener();

    securityAuthority = defaultSecurityAuthority;

    authorizationHandler = new AuthorizationHandlerImpl();
  }

  /**
   * Adds the user interface related to the given protocol provider.
   *
   * @param protocolProvider the protocol provider for which we add the user interface
   */
  public void addProtocolProviderUI(ProtocolProviderService protocolProvider) {
    OperationSetBasicTelephony<?> telOpSet =
        protocolProvider.getOperationSet(OperationSetBasicTelephony.class);

    if (telOpSet != null) {
      telOpSet.addCallListener(androidCallListener);
    }

    OperationSetPresence presenceOpSet =
        protocolProvider.getOperationSet(OperationSetPresence.class);

    if (presenceOpSet != null) {
      presenceOpSet.addProviderPresenceStatusListener(androidPresenceListener);
    }
  }

  /**
   * Removes the user interface related to the given protocol provider.
   *
   * @param protocolProvider the protocol provider to remove
   */
  public void removeProtocolProviderUI(ProtocolProviderService protocolProvider) {
    OperationSetBasicTelephony<?> telOpSet =
        protocolProvider.getOperationSet(OperationSetBasicTelephony.class);

    if (telOpSet != null) {
      telOpSet.removeCallListener(androidCallListener);
    }

    OperationSetPresence presenceOpSet =
        protocolProvider.getOperationSet(OperationSetPresence.class);

    if (presenceOpSet != null) {
      presenceOpSet.removeProviderPresenceStatusListener(androidPresenceListener);
    }

    // Removes all chat session for unregistered provider
    ChatSessionManager.removeAllChatsForProvider(protocolProvider);
  }

  /**
   * Starts the connecting user interface for the given protocol provider.
   *
   * @param protocolProvider the protocol provider for which we add the connecting user interface
   */
  public void startConnectingUI(ProtocolProviderService protocolProvider) {}

  /**
   * Stops the connecting user interface for the given protocol provider.
   *
   * @param protocolProvider the protocol provider for which we remove the connecting user interface
   */
  public void stopConnectingUI(ProtocolProviderService protocolProvider) {}

  /**
   * Indicates that the given protocol provider has been connected at the given time.
   *
   * @param protocolProvider the <tt>ProtocolProviderService</tt> corresponding to the connected
   *     account
   * @param date the date/time at which the account has connected
   */
  public void protocolProviderConnected(ProtocolProviderService protocolProvider, long date) {

    OperationSetPresence presence = AccountStatusUtils.getProtocolPresenceOpSet(protocolProvider);

    if (presence != null) {
      presence.setAuthorizationHandler(authorizationHandler);
    }

    updateGlobalStatus();
  }

  /**
   * Indicates that a protocol provider connection has failed.
   *
   * @param protocolProvider the <tt>ProtocolProviderService</tt>, which connection failed
   * @param loginManagerCallback the <tt>LoginManager</tt> implementation, which is managing the
   *     process
   */
  public void protocolProviderConnectionFailed(
      final ProtocolProviderService protocolProvider, final LoginManager loginManagerCallback) {
    AccountID accountID = protocolProvider.getAccountID();

    AndroidUtils.showAlertConfirmDialog(
        JitsiApplication.getGlobalContext(),
        JitsiApplication.getResString(R.string.service_gui_ERROR),
        JitsiApplication.getResString(
            R.string.service_gui_CONNECTION_FAILED_MSG,
            accountID.getUserID(),
            accountID.getService()),
        JitsiApplication.getResString(R.string.service_gui_RETRY),
        new DialogActivity.DialogListener() {
          public boolean onConfirmClicked(DialogActivity dialog) {
            loginManagerCallback.login(protocolProvider);
            return true;
          }

          public void onDialogCancelled(DialogActivity dialog) {}
        });
  }

  /**
   * Returns the <tt>SecurityAuthority</tt> implementation related to this login renderer.
   *
   * @param protocolProvider the specific <tt>ProtocolProviderService</tt>, for which we're
   *     obtaining a security authority
   * @return the <tt>SecurityAuthority</tt> implementation related to this login renderer
   */
  public SecurityAuthority getSecurityAuthorityImpl(ProtocolProviderService protocolProvider) {
    return securityAuthority;
  }

  /** Updates Jitsi icon notification to reflect current global status. */
  public void updateJitsiIconNotification() {
    String status;
    if (getGlobalStatus().isOnline()) {
      // At least one provider is online
      status = JitsiApplication.getResString(R.string.service_gui_ONLINE);
    } else {
      // There are no active providers so we consider to be in
      // the offline state
      status = JitsiApplication.getResString(R.string.service_gui_OFFLINE);
    }

    int notificationID = OSGiService.getGeneralNotificationId();
    if (notificationID == -1) {
      logger.debug(
          "Not displaying status notification because"
              + " there's no global notification icon available.");
      return;
    }

    AndroidUtils.updateGeneralNotification(
        JitsiApplication.getGlobalContext(),
        notificationID,
        JitsiApplication.getResString(R.string.app_name),
        status,
        System.currentTimeMillis());
  }

  /**
   * Adds global status listener.
   *
   * @param l the listener to be add.
   */
  public void addGlobalStatusListener(EventListener<PresenceStatus> l) {
    globalStatusListeners.addEventListener(l);
  }

  /**
   * Removes global status listener.
   *
   * @param l the listener to remove.
   */
  public void removeGlobalStatusListener(EventListener<PresenceStatus> l) {
    globalStatusListeners.removeEventListener(l);
  }

  /**
   * Returns current global status.
   *
   * @return current global status.
   */
  public PresenceStatus getGlobalStatus() {
    if (globalStatus == null) {
      GlobalStatusService gss = AndroidGUIActivator.getGlobalStatusService();

      globalStatus = gss != null ? gss.getGlobalPresenceStatus() : GlobalStatusEnum.OFFLINE;
    }
    return globalStatus;
  }

  /** AuthorizationHandler instance used by this login renderer. */
  public AuthorizationHandlerImpl getAuthorizationHandler() {
    return authorizationHandler;
  }

  /**
   * Listens for all providerStatusChanged and providerStatusMessageChanged events in order to
   * refresh the account status panel, when a status is changed.
   */
  private class UIProviderPresenceStatusListener implements ProviderPresenceStatusListener {
    public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt) {
      updateGlobalStatus();
    }

    public void providerStatusMessageChanged(PropertyChangeEvent evt) {}
  }

  /**
   * Indicates if the given <tt>protocolProvider</tt> related user interface is already rendered.
   *
   * @param protocolProvider the <tt>ProtocolProviderService</tt>, which related user interface
   *     we're looking for
   * @return <tt>true</tt> if the given <tt>protocolProvider</tt> related user interface is already
   *     rendered
   */
  public boolean containsProtocolProviderUI(ProtocolProviderService protocolProvider) {
    return false;
  }

  /** Updates the global status by picking the most connected protocol provider status. */
  private void updateGlobalStatus() {
    // Only if the GUI is active (bundle context will be null on shutdown)
    if (AndroidGUIActivator.bundleContext != null) {
      // Invalidate local status image
      localStatusRaw = null;
      // Invalidate global status
      globalStatus = null;
      globalStatusListeners.notifyEventListeners(getGlobalStatus());
    }

    updateJitsiIconNotification();
  }

  /**
   * Returns the local user avatar drawable.
   *
   * @return the local user avatar drawable.
   */
  public Drawable getLocalAvatarDrawable() {
    GlobalDisplayDetailsService displayDetailsService =
        AndroidGUIActivator.getGlobalDisplayDetailsService();

    byte[] avatarImage = displayDetailsService.getGlobalDisplayAvatar();
    // Re-create drawable only if avatar has changed
    if (avatarImage != localAvatarRaw) {
      localAvatarRaw = avatarImage;
      localAvatar = AndroidImageUtil.roundedDrawableFromBytes(avatarImage);
    }
    return localAvatar;
  }

  /**
   * Returns the local user status drawable.
   *
   * @return the local user status drawable
   */
  public synchronized Drawable getLocalStatusDrawable() {
    byte[] statusImage = StatusUtil.getContactStatusIcon(getGlobalStatus());
    if (statusImage != localStatusRaw) {
      localStatusRaw = statusImage;
      localStatusDrawable =
          localStatusRaw != null ? AndroidImageUtil.drawableFromBytes(statusImage) : null;
    }
    return localStatusDrawable;
  }
}
/**
 * Implements <tt>BundleActivator</tt> for the neomedia bundle.
 *
 * @author Martin Andre
 * @author Emil Ivov
 * @author Lyubomir Marinov
 * @author Boris Grozev
 */
public class NeomediaActivator implements BundleActivator {

  /**
   * The <tt>Logger</tt> used by the <tt>NeomediaActivator</tt> class and its instances for logging
   * output.
   */
  private final Logger logger = Logger.getLogger(NeomediaActivator.class);

  /** Indicates if the audio configuration form should be disabled, i.e. not visible to the user. */
  private static final String AUDIO_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.AUDIO_CONFIG_DISABLED";

  /** Indicates if the video configuration form should be disabled, i.e. not visible to the user. */
  private static final String VIDEO_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.VIDEO_CONFIG_DISABLED";

  /** Indicates if the H.264 configuration form should be disabled, i.e. not visible to the user. */
  private static final String H264_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.h264config.DISABLED";

  /** Indicates if the ZRTP configuration form should be disabled, i.e. not visible to the user. */
  private static final String ZRTP_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.zrtpconfig.DISABLED";

  /**
   * Indicates if the call recording config form should be disabled, i.e. not visible to the user.
   */
  private static final String CALL_RECORDING_CONFIG_DISABLED_PROP =
      "net.java.sip.communicator.impl.neomedia.callrecordingconfig.DISABLED";

  /**
   * The name of the notification pop-up event displayed when the device configration has changed.
   */
  private static final String DEVICE_CONFIGURATION_HAS_CHANGED = "DeviceConfigurationChanged";

  /**
   * The context in which the one and only <tt>NeomediaActivator</tt> instance has started
   * executing.
   */
  private static BundleContext bundleContext;

  /**
   * The <tt>ConfigurationService</tt> registered in {@link #bundleContext} and used by the
   * <tt>NeomediaActivator</tt> instance to read and write configuration properties.
   */
  private static ConfigurationService configurationService;

  /**
   * The <tt>FileAccessService</tt> registered in {@link #bundleContext} and used by the
   * <tt>NeomediaActivator</tt> instance to safely access files.
   */
  private static FileAccessService fileAccessService;

  /** The notifcation service to pop-up messages. */
  private static NotificationService notificationService;

  /**
   * The one and only <tt>MediaServiceImpl</tt> instance registered in {@link #bundleContext} by the
   * <tt>NeomediaActivator</tt> instance.
   */
  private static MediaServiceImpl mediaServiceImpl;

  /**
   * The <tt>ResourceManagementService</tt> registered in {@link #bundleContext} and representing
   * the resources such as internationalized and localized text and images used by the neomedia
   * bundle.
   */
  private static ResourceManagementService resources;

  /**
   * The OSGi <tt>PacketLoggingService</tt> of {@link #mediaServiceImpl} in {@link #bundleContext}
   * and used for debugging.
   */
  private static PacketLoggingService packetLoggingService = null;

  /** A listener to the click on the popup message concerning device configuration changes. */
  private AudioDeviceConfigurationListener deviceConfigurationPropertyChangeListener;

  /** A {@link MediaConfigurationService} instance. */
  //    private static MediaConfigurationImpl mediaConfiguration;

  /** The audio configuration form used to define the capture/notify/playback audio devices. */
  private static ConfigurationForm audioConfigurationForm;

  /**
   * Starts the execution of the neomedia bundle in the specified context.
   *
   * @param bundleContext the context in which the neomedia bundle is to start executing
   * @throws Exception if an error occurs while starting the execution of the neomedia bundle in the
   *     specified context
   */
  public void start(BundleContext bundleContext) throws Exception {
    if (logger.isDebugEnabled()) logger.debug("Started.");

    NeomediaActivator.bundleContext = bundleContext;

    // MediaService
    mediaServiceImpl = (MediaServiceImpl) LibJitsi.getMediaService();

    bundleContext.registerService(MediaService.class.getName(), mediaServiceImpl, null);
    if (logger.isDebugEnabled()) logger.debug("Media Service ... [REGISTERED]");

    //        mediaConfiguration = new MediaConfigurationImpl();
    //        bundleContext.registerService(
    //                MediaConfigurationService.class.getStatus(),
    //                getMediaConfiguration(),
    //                null);
    if (logger.isDebugEnabled()) logger.debug("Media Configuration ... [REGISTERED]");

    ConfigurationService cfg = NeomediaActivator.getConfigurationService();
    Dictionary<String, String> mediaProps = new Hashtable<String, String>();

    mediaProps.put(ConfigurationForm.FORM_TYPE, ConfigurationForm.GENERAL_TYPE);

    // If the audio configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(AUDIO_CONFIG_DISABLED_PROP, false))
    //        {
    //            audioConfigurationForm
    //                = new LazyConfigurationForm(
    //                        AudioConfigurationPanel.class.getStatus(),
    //                        getClass().getClassLoader(),
    //                        "plugin.mediaconfig.AUDIO_ICON",
    //                        "impl.neomedia.configform.AUDIO",
    //                        3);
    //
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    audioConfigurationForm,
    //                    mediaProps);
    //
    //            if (deviceConfigurationPropertyChangeListener == null)
    //            {
    //                // Initializes and registers the changed device configuration
    //                // event ot the notification service.
    //                getNotificationService();
    //
    //                deviceConfigurationPropertyChangeListener
    //                    = new AudioDeviceConfigurationListener();
    //                mediaServiceImpl
    //                    .getDeviceConfiguration()
    //                        .addPropertyChangeListener(
    //                                deviceConfigurationPropertyChangeListener);
    //            }
    //        }

    // If the video configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(VIDEO_CONFIG_DISABLED_PROP, false))
    //        {
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            VideoConfigurationPanel.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            "plugin.mediaconfig.VIDEO_ICON",
    //                            "impl.neomedia.configform.VIDEO",
    //                            4),
    //                    mediaProps);
    //        }

    // H.264
    // If the H.264 configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(H264_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> h264Props
    //                = new Hashtable<String, String>();
    //
    //            h264Props.put(
    //                    ConfigurationForm.FORM_TYPE,
    //                    ConfigurationForm.ADVANCED_TYPE);
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            ConfigurationPanel.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            "plugin.mediaconfig.VIDEO_ICON",
    //                            "impl.neomedia.configform.H264",
    //                            -1,
    //                            true),
    //                    h264Props);
    //        }

    // ZRTP
    // If the ZRTP configuration form is disabled don't register it.
    //        if ((cfg == null) || !cfg.getBoolean(ZRTP_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> securityProps
    //                = new Hashtable<String, String>();
    //
    //            securityProps.put( ConfigurationForm.FORM_TYPE,
    //                            ConfigurationForm.SECURITY_TYPE);
    //            bundleContext.registerService(
    //                ConfigurationForm.class.getStatus(),
    //                new LazyConfigurationForm(
    //                    SecurityConfigForm.class.getStatus(),
    //                    getClass().getClassLoader(),
    //                    "impl.media.security.zrtp.CONF_ICON",
    //                    "impl.media.security.zrtp.TITLE",
    //                    0),
    //                securityProps);
    //        }

    // we use the nist-sdp stack to make parse sdp and we need to set the
    // following property to make sure that it would accept java generated
    // IPv6 addresses that contain address scope zones.
    System.setProperty("gov.nist.core.STRIP_ADDR_SCOPES", "true");

    // AudioNotifierService
    AudioNotifierService audioNotifierService = LibJitsi.getAudioNotifierService();

    audioNotifierService.setMute(
        (cfg == null)
            || !cfg.getBoolean("net.java.sip.communicator" + ".impl.sound.isSoundEnabled", true));
    bundleContext.registerService(AudioNotifierService.class.getName(), audioNotifierService, null);

    if (logger.isInfoEnabled()) logger.info("Audio Notifier Service ...[REGISTERED]");

    // Call Recording
    // If the call recording configuration form is disabled don't continue.
    //        if ((cfg == null)
    //                || !cfg.getBoolean(CALL_RECORDING_CONFIG_DISABLED_PROP, false))
    //        {
    //            Dictionary<String, String> callRecordingProps
    //                = new Hashtable<String, String>();
    //
    //            callRecordingProps.put(
    //                    ConfigurationForm.FORM_TYPE,
    //                    ConfigurationForm.ADVANCED_TYPE);
    //            bundleContext.registerService(
    //                    ConfigurationForm.class.getStatus(),
    //                    new LazyConfigurationForm(
    //                            CallRecordingConfigForm.class.getStatus(),
    //                            getClass().getClassLoader(),
    //                            null,
    //                            "plugin.callrecordingconfig.CALL_RECORDING_CONFIG",
    //                            1100,
    //                            true),
    //                    callRecordingProps);
    //        }
  }

  /**
   * Stops the execution of the neomedia bundle in the specified context.
   *
   * @param bundleContext the context in which the neomedia bundle is to stop executing
   * @throws Exception if an error occurs while stopping the execution of the neomedia bundle in the
   *     specified context
   */
  public void stop(BundleContext bundleContext) throws Exception {
    try {
      if (deviceConfigurationPropertyChangeListener != null) {
        mediaServiceImpl
            .getDeviceConfiguration()
            .removePropertyChangeListener(deviceConfigurationPropertyChangeListener);
        if (deviceConfigurationPropertyChangeListener != null) {
          deviceConfigurationPropertyChangeListener.managePopupMessageListenerRegistration(false);
          deviceConfigurationPropertyChangeListener = null;
        }
      }
    } finally {
      configurationService = null;
      fileAccessService = null;
      mediaServiceImpl = null;
      resources = null;
    }
  }

  /**
   * Returns a reference to a ConfigurationService implementation currently registered in the bundle
   * context or null if no such implementation was found.
   *
   * @return a currently valid implementation of the ConfigurationService.
   */
  public static ConfigurationService getConfigurationService() {
    if (configurationService == null) {
      configurationService = ServiceUtils.getService(bundleContext, ConfigurationService.class);
    }
    return configurationService;
  }

  /**
   * Returns a reference to a FileAccessService implementation currently registered in the bundle
   * context or null if no such implementation was found.
   *
   * @return a currently valid implementation of the FileAccessService .
   */
  public static FileAccessService getFileAccessService() {
    if (fileAccessService == null) {
      fileAccessService = ServiceUtils.getService(bundleContext, FileAccessService.class);
    }
    return fileAccessService;
  }

  /**
   * Gets the <tt>MediaService</tt> implementation instance registered by the neomedia bundle.
   *
   * @return the <tt>MediaService</tt> implementation instance registered by the neomedia bundle
   */
  public static MediaServiceImpl getMediaServiceImpl() {
    return mediaServiceImpl;
  }

  //    public static MediaConfigurationService getMediaConfiguration()
  //    {
  //        return mediaConfiguration;
  //    }

  /**
   * Gets the <tt>ResourceManagementService</tt> instance which represents the resources such as
   * internationalized and localized text and images used by the neomedia bundle.
   *
   * @return the <tt>ResourceManagementService</tt> instance which represents the resources such as
   *     internationalized and localized text and images used by the neomedia bundle
   */
  public static ResourceManagementService getResources() {
    if (resources == null) {
      resources = ResourceManagementServiceUtils.getService(bundleContext);
    }
    return resources;
  }

  /**
   * Returns a reference to the <tt>PacketLoggingService</tt> implementation currently registered in
   * the bundle context or null if no such implementation was found.
   *
   * @return a reference to a <tt>PacketLoggingService</tt> implementation currently registered in
   *     the bundle context or null if no such implementation was found.
   */
  public static PacketLoggingService getPacketLogging() {
    if (packetLoggingService == null) {
      packetLoggingService = ServiceUtils.getService(bundleContext, PacketLoggingService.class);
    }
    return packetLoggingService;
  }

  /**
   * Returns the <tt>NotificationService</tt> obtained from the bundle context.
   *
   * @return The <tt>NotificationService</tt> obtained from the bundle context.
   */
  public static NotificationService getNotificationService() {
    if (notificationService == null) {
      // Get the notification service implementation
      ServiceReference notifReference =
          bundleContext.getServiceReference(NotificationService.class.getName());

      notificationService = (NotificationService) bundleContext.getService(notifReference);

      if (notificationService != null) {
        // Register a popup message for a device configuration changed
        // notification.
        notificationService.registerDefaultNotificationForEvent(
            DEVICE_CONFIGURATION_HAS_CHANGED,
            net.java.sip.communicator.service.notification.NotificationAction.ACTION_POPUP_MESSAGE,
            "Device onfiguration has changed",
            null);
      }
    }

    return notificationService;
  }

  /** A listener to the click on the popup message concerning device configuration changes. */
  private class AudioDeviceConfigurationListener implements PropertyChangeListener /*,
                   SystrayPopupMessageListener*/ {
    /**
     * A boolean used to verify that this listener registers only once to the popup message
     * notification handler.
     */
    private boolean isRegisteredToPopupMessageListener = false;

    /**
     * Registers or unregister as a popup message listener to detect when a user click on
     * notification saying that the device configuration has changed.
     *
     * @param enable True to register to the popup message notifcation handler. False to unregister.
     */
    public void managePopupMessageListenerRegistration(boolean enable) {
      Iterator<NotificationHandler> notificationHandlers =
          notificationService
              .getActionHandlers(
                  net.java.sip.communicator.service.notification.NotificationAction
                      .ACTION_POPUP_MESSAGE)
              .iterator();
      NotificationHandler notificationHandler;
      while (notificationHandlers.hasNext()) {
        notificationHandler = notificationHandlers.next();
        if (notificationHandler instanceof PopupMessageNotificationHandler) {
          // Register.
          if (enable) {
            //                        ((PopupMessageNotificationHandler) notificationHandler)
            //                            .addPopupMessageListener(this);
          }
          // Unregister.
          else {
            //                        ((PopupMessageNotificationHandler) notificationHandler)
            //                            .removePopupMessageListener(this);
          }
        }
      }
    }

    /**
     * Function called when an audio device is plugged or unplugged.
     *
     * @param event The property change event which may concern the audio device.
     */
    public void propertyChange(PropertyChangeEvent event) {
      if (DeviceConfiguration.PROP_AUDIO_SYSTEM_DEVICES.equals(event.getPropertyName())) {
        NotificationService notificationService = getNotificationService();

        if (notificationService != null) {
          // Registers only once to the  popup message notification
          // handler.
          if (!isRegisteredToPopupMessageListener) {
            isRegisteredToPopupMessageListener = true;
            managePopupMessageListenerRegistration(true);
          }

          // Fires the popup notification.
          ResourceManagementService resources = NeomediaActivator.getResources();
          Map<String, Object> extras = new HashMap<String, Object>();

          extras.put(NotificationData.POPUP_MESSAGE_HANDLER_TAG_EXTRA, this);
          notificationService.fireNotification(
              DEVICE_CONFIGURATION_HAS_CHANGED,
              resources.getI18NString("impl.media.configform" + ".AUDIO_DEVICE_CONFIG_CHANGED"),
              resources.getI18NString(
                  "impl.media.configform" + ".AUDIO_DEVICE_CONFIG_MANAGMENT_CLICK"),
              null,
              extras);
        }
      }
    }

    /**
     * Indicates that user has clicked on the systray popup message.
     *
     * @param evt the event triggered when user clicks on the systray popup message
     */
    //        public void popupMessageClicked(SystrayPopupMessageEvent evt)
    //        {
    //            // Checks if this event is fired from one click on one of our popup
    //            // message.
    //            if(evt.getTag() == deviceConfigurationPropertyChangeListener)
    //            {
    //                // Get the UI service
    //                ServiceReference uiReference = bundleContext
    //                    .getServiceReference(UIService.class.getStatus());
    //
    //                UIService uiService = (UIService) bundleContext
    //                    .getService(uiReference);
    //
    //                if(uiService != null)
    //                {
    //                    // Shows the audio configuration window.
    //                    ConfigurationContainer configurationContainer
    //                        = uiService.getConfigurationContainer();
    //                    configurationContainer.setSelected(audioConfigurationForm);
    //                    configurationContainer.setVisible(true);
    //                }
    //            }
    //        }
  }

  public static BundleContext getBundleContext() {
    return bundleContext;
  }
}