001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.GridBagLayout;
008import java.net.Authenticator.RequestorType;
009import java.util.concurrent.Executors;
010import java.util.concurrent.ScheduledExecutorService;
011import java.util.concurrent.ScheduledFuture;
012import java.util.concurrent.TimeUnit;
013
014import javax.swing.JLabel;
015import javax.swing.JOptionPane;
016import javax.swing.JPanel;
017
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.data.osm.UserInfo;
020import org.openstreetmap.josm.data.preferences.BooleanProperty;
021import org.openstreetmap.josm.data.preferences.IntegerProperty;
022import org.openstreetmap.josm.gui.JosmUserIdentityManager;
023import org.openstreetmap.josm.gui.Notification;
024import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
025import org.openstreetmap.josm.gui.util.GuiHelper;
026import org.openstreetmap.josm.gui.widgets.UrlLabel;
027import org.openstreetmap.josm.io.auth.CredentialsAgentException;
028import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
029import org.openstreetmap.josm.io.auth.CredentialsManager;
030import org.openstreetmap.josm.io.auth.JosmPreferencesCredentialAgent;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.Utils;
033
034/**
035 * Notifies user periodically of new received (unread) messages
036 * @since 6349
037 */
038public final class MessageNotifier {
039
040    private MessageNotifier() {
041        // Hide default constructor for utils classes
042    }
043
044    /** Property defining if this task is enabled or not */
045    public static final BooleanProperty PROP_NOTIFIER_ENABLED = new BooleanProperty("message.notifier.enabled", true);
046    /** Property defining the update interval in minutes */
047    public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("message.notifier.interval", 5);
048
049    private static final ScheduledExecutorService EXECUTOR =
050            Executors.newSingleThreadScheduledExecutor(Utils.newThreadFactory("message-notifier-%d", Thread.NORM_PRIORITY));
051
052    private static final Runnable WORKER = new Worker();
053
054    private static volatile ScheduledFuture<?> task;
055
056    private static class Worker implements Runnable {
057
058        private int lastUnreadCount;
059
060        @Override
061        public void run() {
062            try {
063                final UserInfo userInfo = new OsmServerUserInfoReader().fetchUserInfo(NullProgressMonitor.INSTANCE,
064                        tr("get number of unread messages"));
065                final int unread = userInfo.getUnreadMessages();
066                if (unread > 0 && unread != lastUnreadCount) {
067                    GuiHelper.runInEDT(() -> {
068                        JPanel panel = new JPanel(new GridBagLayout());
069                        panel.add(new JLabel(trn("You have {0} unread message.", "You have {0} unread messages.", unread, unread)),
070                                GBC.eol());
071                        panel.add(new UrlLabel(Main.getBaseUserUrl() + '/' + userInfo.getDisplayName() + "/inbox",
072                                tr("Click here to see your inbox.")), GBC.eol());
073                        panel.setOpaque(false);
074                        new Notification().setContent(panel)
075                            .setIcon(JOptionPane.INFORMATION_MESSAGE)
076                            .setDuration(Notification.TIME_LONG)
077                            .show();
078                    });
079                    lastUnreadCount = unread;
080                }
081            } catch (OsmTransferException e) {
082                Main.warn(e);
083            }
084        }
085    }
086
087    /**
088     * Starts the message notifier task if not already started and if user is fully identified
089     */
090    public static void start() {
091        int interval = PROP_INTERVAL.get();
092        if (Main.isOffline(OnlineResource.OSM_API)) {
093            Main.info(tr("{0} not available (offline mode)", tr("Message notifier")));
094        } else if (!isRunning() && interval > 0 && isUserEnoughIdentified()) {
095            task = EXECUTOR.scheduleAtFixedRate(WORKER, 0, TimeUnit.MINUTES.toSeconds(interval), TimeUnit.SECONDS);
096            Main.info("Message notifier active (checks every "+interval+" minute"+(interval > 1 ? "s" : "")+')');
097        }
098    }
099
100    /**
101     * Stops the message notifier task if started
102     */
103    public static void stop() {
104        if (isRunning()) {
105            task.cancel(false);
106            Main.info("Message notifier inactive");
107            task = null;
108        }
109    }
110
111    /**
112     * Determines if the message notifier is currently running
113     * @return {@code true} if the notifier is running, {@code false} otherwise
114     */
115    public static boolean isRunning() {
116        return task != null;
117    }
118
119    /**
120     * Determines if user set enough information in JOSM preferences to make the request to OSM API without
121     * prompting him for a password.
122     * @return {@code true} if user chose an OAuth token or supplied both its username and password, {@code false otherwise}
123     */
124    public static boolean isUserEnoughIdentified() {
125        JosmUserIdentityManager identManager = JosmUserIdentityManager.getInstance();
126        if (identManager.isFullyIdentified()) {
127            return true;
128        } else {
129            CredentialsManager credManager = CredentialsManager.getInstance();
130            try {
131                if (JosmPreferencesCredentialAgent.class.equals(credManager.getCredentialsAgentClass())) {
132                    if (OsmApi.isUsingOAuth()) {
133                        return credManager.lookupOAuthAccessToken() != null;
134                    } else {
135                        String username = Main.pref.get("osm-server.username", null);
136                        String password = Main.pref.get("osm-server.password", null);
137                        return username != null && !username.isEmpty() && password != null && !password.isEmpty();
138                    }
139                } else {
140                    CredentialsAgentResponse credentials = credManager.getCredentials(
141                            RequestorType.SERVER, OsmApi.getOsmApi().getHost(), false);
142                    if (credentials != null) {
143                        String username = credentials.getUsername();
144                        char[] password = credentials.getPassword();
145                        return username != null && !username.isEmpty() && password != null && password.length > 0;
146                    }
147                }
148            } catch (CredentialsAgentException e) {
149                Main.warn(e, "Unable to get credentials:");
150            }
151        }
152        return false;
153    }
154}