001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.net.Authenticator.RequestorType;
007import java.util.concurrent.Executors;
008import java.util.concurrent.ScheduledExecutorService;
009import java.util.concurrent.ScheduledFuture;
010import java.util.concurrent.TimeUnit;
011
012import org.openstreetmap.josm.data.UserIdentityManager;
013import org.openstreetmap.josm.data.osm.UserInfo;
014import org.openstreetmap.josm.data.preferences.BooleanProperty;
015import org.openstreetmap.josm.data.preferences.IntegerProperty;
016import org.openstreetmap.josm.gui.ExceptionDialogUtil;
017import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
018import org.openstreetmap.josm.io.auth.CredentialsAgentException;
019import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
020import org.openstreetmap.josm.io.auth.CredentialsManager;
021import org.openstreetmap.josm.io.auth.JosmPreferencesCredentialAgent;
022import org.openstreetmap.josm.spi.preferences.Config;
023import org.openstreetmap.josm.tools.Logging;
024import org.openstreetmap.josm.tools.Utils;
025
026/**
027 * Notifies user periodically of new received (unread) messages
028 * @since 6349
029 */
030public final class MessageNotifier {
031
032    private MessageNotifier() {
033        // Hide default constructor for utils classes
034    }
035
036    /**
037     * Called when new new messages are detected.
038     * @since 12766
039     */
040    @FunctionalInterface
041    public interface NotifierCallback {
042        /**
043         * Perform the actual notification of new messages.
044         * @param userInfo the new user information, that includes the number of unread messages
045         */
046        void notifyNewMessages(UserInfo userInfo);
047    }
048
049    private static volatile NotifierCallback callback;
050
051    /**
052     * Sets the {@link NotifierCallback} responsible of notifying the user when new messages are received.
053     * @param notifierCallback the new {@code NotifierCallback}
054     */
055    public static void setNotifierCallback(NotifierCallback notifierCallback) {
056        callback = notifierCallback;
057    }
058
059    /** Property defining if this task is enabled or not */
060    public static final BooleanProperty PROP_NOTIFIER_ENABLED = new BooleanProperty("message.notifier.enabled", true);
061    /** Property defining the update interval in minutes */
062    public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("message.notifier.interval", 5);
063
064    private static final ScheduledExecutorService EXECUTOR =
065            Executors.newSingleThreadScheduledExecutor(Utils.newThreadFactory("message-notifier-%d", Thread.NORM_PRIORITY));
066
067    private static final Runnable WORKER = new Worker();
068
069    private static volatile ScheduledFuture<?> task;
070
071    private static class Worker implements Runnable {
072
073        private int lastUnreadCount;
074        private long lastTimeInMillis;
075
076        @Override
077        public void run() {
078            try {
079                long currentTime = System.currentTimeMillis();
080                // See #14671 - Make sure we don't run the API call many times after system wakeup
081                if (currentTime >= lastTimeInMillis + TimeUnit.MINUTES.toMillis(PROP_INTERVAL.get())) {
082                    lastTimeInMillis = currentTime;
083                    final UserInfo userInfo = new OsmServerUserInfoReader().fetchUserInfo(NullProgressMonitor.INSTANCE,
084                            tr("get number of unread messages"));
085                    final int unread = userInfo.getUnreadMessages();
086                    if (unread > 0 && unread != lastUnreadCount) {
087                        callback.notifyNewMessages(userInfo);
088                        lastUnreadCount = unread;
089                    }
090                }
091            } catch (OsmTransferException e) {
092                ExceptionDialogUtil.explainOsmTransferException(e);
093            }
094        }
095    }
096
097    /**
098     * Starts the message notifier task if not already started and if user is fully identified
099     */
100    public static void start() {
101        int interval = PROP_INTERVAL.get();
102        if (NetworkManager.isOffline(OnlineResource.OSM_API)) {
103            Logging.info(tr("{0} not available (offline mode)", tr("Message notifier")));
104        } else if (!isRunning() && interval > 0 && isUserEnoughIdentified()) {
105            task = EXECUTOR.scheduleAtFixedRate(WORKER, 0, interval, TimeUnit.MINUTES);
106            Logging.info("Message notifier active (checks every "+interval+" minute"+(interval > 1 ? "s" : "")+')');
107        }
108    }
109
110    /**
111     * Stops the message notifier task if started
112     */
113    public static void stop() {
114        if (isRunning()) {
115            task.cancel(false);
116            Logging.info("Message notifier inactive");
117            task = null;
118        }
119    }
120
121    /**
122     * Determines if the message notifier is currently running
123     * @return {@code true} if the notifier is running, {@code false} otherwise
124     */
125    public static boolean isRunning() {
126        return task != null;
127    }
128
129    /**
130     * Determines if user set enough information in JOSM preferences to make the request to OSM API without
131     * prompting him for a password.
132     * @return {@code true} if user chose an OAuth token or supplied both its username and password, {@code false otherwise}
133     */
134    public static boolean isUserEnoughIdentified() {
135        UserIdentityManager identManager = UserIdentityManager.getInstance();
136        if (identManager.isFullyIdentified()) {
137            return true;
138        } else {
139            CredentialsManager credManager = CredentialsManager.getInstance();
140            try {
141                if (JosmPreferencesCredentialAgent.class.equals(credManager.getCredentialsAgentClass())) {
142                    if (OsmApi.isUsingOAuth()) {
143                        return credManager.lookupOAuthAccessToken() != null;
144                    } else {
145                        String username = Config.getPref().get("osm-server.username", null);
146                        String password = Config.getPref().get("osm-server.password", null);
147                        return username != null && !username.isEmpty() && password != null && !password.isEmpty();
148                    }
149                } else {
150                    CredentialsAgentResponse credentials = credManager.getCredentials(
151                            RequestorType.SERVER, OsmApi.getOsmApi().getHost(), false);
152                    if (credentials != null) {
153                        String username = credentials.getUsername();
154                        char[] password = credentials.getPassword();
155                        return username != null && !username.isEmpty() && password != null && password.length > 0;
156                    }
157                }
158            } catch (CredentialsAgentException e) {
159                Logging.log(Logging.LEVEL_WARN, "Unable to get credentials:", e);
160            }
161        }
162        return false;
163    }
164}