/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.plugin.otr.authdialog; import java.awt.*; import java.awt.event.*; import java.io.*; import java.security.*; import java.util.*; import javax.imageio.*; import javax.swing.*; import javax.swing.Timer; import net.java.otr4j.session.*; import net.java.sip.communicator.plugin.desktoputil.*; import net.java.sip.communicator.plugin.otr.*; import net.java.sip.communicator.plugin.otr.OtrContactManager.OtrContact; import net.java.sip.communicator.service.contactlist.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.gui.Container; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; /** * A special {@link JMenuBar} that controls the switching of OTRv3 outgoing * sessions in case the remote party is logged in multiple times. * * @author Marin Dzhigarov * */ public class OTRv3OutgoingSessionSwitcher extends SIPCommMenuBar implements PluginComponent, ActionListener, ScOtrEngineListener, ScOtrKeyManagerListener { private static final Logger logger = Logger.getLogger(OTRv3OutgoingSessionSwitcher.class); private final PluginComponentFactory parentFactory; private static final long serialVersionUID = 0L; private final SelectorMenu menu = new SelectorMenu(); private ButtonGroup buttonGroup = new ButtonGroup(); private OtrContact contact; /** * A map used for storing each <tt>Session</tt>s corresponding <tt>JMenuItem * </tt>. */ private final Map<Session, JMenuItem> outgoingSessions = new HashMap<Session, JMenuItem>(); /** * An animated {@link JMenu} * @author Marin Dzhigarov * */ private static class SelectorMenu extends SIPCommMenu { /** * Serial version UID. */ private static final long serialVersionUID = 0L; Image image = OtrActivator.resourceService.getImage( "service.gui.icons.DOWN_ARROW_ICON").getImage(); private static float alpha = 0.95f; private final Timer alphaChanger = new Timer(20, new ActionListener() { private float incrementer = -.03f; private int fadeCycles = 0; @Override public void actionPerformed(ActionEvent e) { float newAlpha = alpha + incrementer; if (newAlpha < 0.2f) { newAlpha = 0.2f; incrementer = -incrementer; } else if (newAlpha > 0.85f) { newAlpha = 0.85f; incrementer = -incrementer; fadeCycles++; } alpha = newAlpha; if (fadeCycles >= 3) { alphaChanger.stop(); fadeCycles = 0; alpha = 1f; } SelectorMenu.this.repaint(); } }); @Override public void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.setComposite( AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha)); g.drawImage(image, getWidth() - image.getWidth(this) - 1, (getHeight() - image.getHeight(this) - 1) / 2, this); super.paintComponent(g2d); } /** * Creates a fade in and out effect for this {@link JMenu} */ public void fadeAnimation() { alphaChanger.stop(); alpha = 0.85f; SelectorMenu.this.repaint(); alphaChanger.start(); } }; /** * The OTRv3OutgoingSessionSwitcher constructor */ public OTRv3OutgoingSessionSwitcher(Container container, PluginComponentFactory parentFactory) { this.parentFactory = parentFactory; setPreferredSize(new Dimension(30, 28)); setMaximumSize(new Dimension(30, 28)); setMinimumSize(new Dimension(30, 28)); this.menu.setPreferredSize(new Dimension(30, 45)); this.menu.setMaximumSize(new Dimension(30, 45)); this.add(menu); this.setBorder(null); this.menu.setBorder(null); this.menu.setOpaque(false); this.setOpaque(false); this.menu.setVisible(false); /* * XXX This OtrV3OutgoingSessionSwitcher instance cannot be added as a * listener to scOtrEngine and scOtrKeyManager without being removed * later on because the latter live forever. Unfortunately, the * dispose() method of this instance is never executed. OtrWeakListener * will keep this instance as a listener of scOtrEngine and * scOtrKeyManager for as long as this instance is necessary. And this * instance will be strongly referenced by the JMenuItems which depict * it. So when the JMenuItems are gone, this instance will become * obsolete and OtrWeakListener will remove it as a listener of * scOtrEngine and scOtrKeyManager. */ new OtrWeakListener<OTRv3OutgoingSessionSwitcher>( this, OtrActivator.scOtrEngine, OtrActivator.scOtrKeyManager); try { finishedPadlockImage = new ImageIcon(ImageIO.read( OtrActivator.resourceService.getImageURL( "plugin.otr.FINISHED_ICON_BLACK_16x16"))); verifiedLockedPadlockImage = new ImageIcon(ImageIO.read( OtrActivator.resourceService.getImageURL( "plugin.otr.ENCRYPTED_ICON_BLACK_16x16"))); unverifiedLockedPadlockImage = new ImageIcon(ImageIO.read( OtrActivator.resourceService.getImageURL( "plugin.otr.ENCRYPTED_UNVERIFIED_ICON_BLACK_16x16"))); unlockedPadlockImage = new ImageIcon(ImageIO.read( OtrActivator.resourceService.getImageURL( "plugin.otr.PLAINTEXT_ICON_16x16"))); } catch (IOException e) { logger.debug("Failed to load padlock image"); } buildMenu(contact); } @Override public int getPositionIndex() { return -1; } /** * Sets the current contact. Meant to be used by plugin components that * are interested of the current contact. The current contact is the contact * for the currently selected chat transport. * * @param contact the current contact */ public void setCurrentContact(Contact contact) { if (this.contact != null && this.contact.contact == contact) return; this.contact = OtrContactManager.getOtrContact(contact, null); buildMenu(this.contact); } /** * Sets the current meta contact. Meant to be used by plugin components that * are interested of the current contact. The current contact could be the * contact currently selected in the contact list or the contact for the * currently selected chat, etc. It depends on the container, where this * component is meant to be added. * * @param metaContact the current meta contact */ public void setCurrentContact(MetaContact metaContact) { setCurrentContact((metaContact == null) ? null : metaContact .getDefaultContact()); } /** * Sets the current contact. Meant to be used by plugin components that * are interested of the current contact. The current contact is the contact * for the currently selected chat transport. * * @param contact the current contact * @param resourceName the <tt>ContactResource</tt> name. Some components * may be interested in a particular ContactResource of a contact. */ public void setCurrentContact(Contact contact, String resourceName) { if (resourceName == null) { this.contact = OtrContactManager.getOtrContact(contact, null); buildMenu(this.contact); } else { for (ContactResource resource : contact.getResources()) { if (resource.getResourceName().equals(resourceName)) { OtrContact otrContact = OtrContactManager.getOtrContact(contact, resource); if (this.contact == otrContact) return; this.contact = otrContact; buildMenu(this.contact); } } } } @Override public void setCurrentContactGroup(MetaContactGroup metaGroup) {} @Override public void setCurrentAccountID(AccountID accountID) {} @Override public PluginComponentFactory getParentFactory() { return parentFactory; } /** * Implements ScOtrKeyManagerListener#contactVerificationStatusChanged( * Contact). */ public void contactVerificationStatusChanged(OtrContact contact) { buildMenu(contact); if (this.menu.isVisible()) this.menu.fadeAnimation(); } /** * Implements ScOtrEngineListener#contactPolicyChanged(Contact). */ public void contactPolicyChanged(Contact contact) {} /** * Implements ScOtrKeyManagerListener#globalPolicyChanged(). */ public void globalPolicyChanged() {} /** * Implements ScOtrEngineListener#sessionStatusChanged(OtrContact). */ public void sessionStatusChanged(OtrContact contact) { buildMenu(contact); if (this.menu.isVisible()) this.menu.fadeAnimation(); } /** * Implements ScOtrEngineListener#multipleInstancesDetected(OtrContact). */ public void multipleInstancesDetected(OtrContact contact) { buildMenu(contact); if (this.menu.isVisible()) this.menu.fadeAnimation(); } /** * Implements ScOtrEngineListener#outgoingSessionChanged(OtrContact). */ public void outgoingSessionChanged(OtrContact contact) { buildMenu(contact); } private ImageIcon verifiedLockedPadlockImage; private ImageIcon unverifiedLockedPadlockImage; private ImageIcon finishedPadlockImage; private ImageIcon unlockedPadlockImage; /** * Builds the JMenu used for switching between outgoing OTRv3 Sessions in * case the remote party is logged in multiple locations * * @param otrContact the contact which is logged in multiple locations */ private void buildMenu(OtrContact otrContact) { if (otrContact == null || !this.contact.equals(otrContact)) { return; } menu.removeAll(); java.util.List<Session> multipleInstances = OtrActivator.scOtrEngine.getSessionInstances( otrContact); Session outgoingSession = OtrActivator.scOtrEngine.getOutgoingSession(otrContact); int index = 0; for (Session session : multipleInstances) { index++; if (!outgoingSessions.containsKey(session)) { JMenuItem menuItem = new JRadioButtonMenuItem(); outgoingSessions.put(session, menuItem); menuItem.addActionListener(this); } JMenuItem menuItem = outgoingSessions.get(session); menuItem.setText("Session " + index); ImageIcon imageIcon = null; switch (session.getSessionStatus(session.getReceiverInstanceTag())) { case ENCRYPTED: PublicKey pubKey = session.getRemotePublicKey(session.getReceiverInstanceTag()); String fingerprint = OtrActivator.scOtrKeyManager. getFingerprintFromPublicKey(pubKey); imageIcon = OtrActivator.scOtrKeyManager.isVerified( otrContact.contact, fingerprint) ? verifiedLockedPadlockImage : unverifiedLockedPadlockImage; break; case FINISHED: imageIcon = finishedPadlockImage; break; case PLAINTEXT: imageIcon = unlockedPadlockImage; break; } menuItem.setIcon(imageIcon); menu.add(menuItem); SelectedObject selectedObject = new SelectedObject(imageIcon, session); buttonGroup.add(menuItem); menuItem.repaint(); if (session == outgoingSession) { this.menu.setSelected(selectedObject); setSelected(menu.getItem(index - 1)); } } updateEnableStatus(); menu.repaint(); } public void actionPerformed(ActionEvent e) { for (Map.Entry<Session, JMenuItem> entry : outgoingSessions.entrySet()) { JMenuItem menuItem = (JRadioButtonMenuItem) e.getSource(); if (menuItem.equals(entry.getValue())) { OtrActivator.scOtrEngine.setOutgoingSession( contact, entry.getKey().getReceiverInstanceTag()); break; } } } /** * Sets the menu visibility. The menu is visible as soon as it * contains two or more items. If it is empty, it is invisible. */ private void updateEnableStatus() { this.menu.setEnabled(this.menu.getItemCount() > 1); this.menu.setVisible(true); } }