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}