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