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.Main; 013import org.openstreetmap.josm.data.UserIdentityManager; 014import org.openstreetmap.josm.data.osm.UserInfo; 015import org.openstreetmap.josm.data.preferences.BooleanProperty; 016import org.openstreetmap.josm.data.preferences.IntegerProperty; 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 Logging.warn(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 (Main.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, TimeUnit.MINUTES.toSeconds(interval), TimeUnit.SECONDS); 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}