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 (OsmApiException e) {
092                // We want to explicitly display message to user in some cases like when he has been blocked (#17722)
093                ExceptionDialogUtil.explainOsmTransferException(e);
094            } catch (OsmTransferException e) {
095                // But not message for random network or API issues (like in #17929)
096                Logging.warn(e);
097            }
098        }
099    }
100
101    /**
102     * Starts the message notifier task if not already started and if user is fully identified
103     */
104    public static void start() {
105        int interval = PROP_INTERVAL.get();
106        if (NetworkManager.isOffline(OnlineResource.OSM_API)) {
107            Logging.info(OfflineAccessException.forResource(tr("Message notifier")).getMessage());
108        } else if (!isRunning() && interval > 0 && isUserEnoughIdentified()) {
109            task = EXECUTOR.scheduleAtFixedRate(WORKER, 0, interval, TimeUnit.MINUTES);
110            Logging.info("Message notifier active (checks every "+interval+" minute"+(interval > 1 ? "s" : "")+')');
111        }
112    }
113
114    /**
115     * Stops the message notifier task if started
116     */
117    public static void stop() {
118        if (isRunning()) {
119            task.cancel(false);
120            Logging.info("Message notifier inactive");
121            task = null;
122        }
123    }
124
125    /**
126     * Determines if the message notifier is currently running
127     * @return {@code true} if the notifier is running, {@code false} otherwise
128     */
129    public static boolean isRunning() {
130        return task != null;
131    }
132
133    /**
134     * Determines if user set enough information in JOSM preferences to make the request to OSM API without
135     * prompting him for a password.
136     * @return {@code true} if user chose an OAuth token or supplied both its username and password, {@code false otherwise}
137     */
138    public static boolean isUserEnoughIdentified() {
139        UserIdentityManager identManager = UserIdentityManager.getInstance();
140        if (identManager.isFullyIdentified()) {
141            return true;
142        } else {
143            CredentialsManager credManager = CredentialsManager.getInstance();
144            try {
145                if (JosmPreferencesCredentialAgent.class.equals(credManager.getCredentialsAgentClass())) {
146                    if (OsmApi.isUsingOAuth()) {
147                        return credManager.lookupOAuthAccessToken() != null;
148                    } else {
149                        String username = Config.getPref().get("osm-server.username", null);
150                        String password = Config.getPref().get("osm-server.password", null);
151                        return !Utils.isEmpty(username) && !Utils.isEmpty(password);
152                    }
153                } else {
154                    CredentialsAgentResponse credentials = credManager.getCredentials(
155                            RequestorType.SERVER, OsmApi.getOsmApi().getHost(), false);
156                    if (credentials != null) {
157                        String username = credentials.getUsername();
158                        char[] password = credentials.getPassword();
159                        return !Utils.isEmpty(username) && password != null && password.length > 0;
160                    }
161                }
162            } catch (CredentialsAgentException e) {
163                Logging.log(Logging.LEVEL_WARN, "Unable to get credentials:", e);
164            }
165        }
166        return false;
167    }
168}