diff --git a/app/src/main/java/net/moasdawiki/app/MainActivity.java b/app/src/main/java/net/moasdawiki/app/MainActivity.java index e9cc3d1..f2bc43d 100644 --- a/app/src/main/java/net/moasdawiki/app/MainActivity.java +++ b/app/src/main/java/net/moasdawiki/app/MainActivity.java @@ -159,6 +159,7 @@ public class MainActivity extends AppCompatActivity { // determine URL path Uri uri = request.getUrl(); String encodedUrl = uri.toString(); + //noinspection CharsetObjectCanBeUsed String url = URLDecoder.decode(encodedUrl, "UTF-8"); if (!url.startsWith(SERVER_BASE_URL)) { return null; @@ -184,7 +185,7 @@ public class MainActivity extends AppCompatActivity { } InputStream responseData = new ByteArrayInputStream(response.getContent()); return new WebResourceResponse(mimeType, "UTF-8", responseData); - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); } @@ -345,24 +346,33 @@ public class MainActivity extends AppCompatActivity { * This method is run in an asynchronous background thread. */ private void runSynchronizationWithServer() { - int filesCount; + SynchronizeWikiClient.SyncResult syncResult; try { - filesCount = synchronizeWikiClient.synchronizeRepository(this::syncProgress); + syncResult = synchronizeWikiClient.synchronizeRepository(this::syncProgress); } catch (ServiceException e) { Log.e(TAG, "Error synchronizing repository with server", e); - runOnUiThread(() -> showToast(getString(R.string.settings_synchronize_failed))); + runOnUiThread(() -> showToast(getString(R.string.synchronize_failed))); return; } - if (filesCount > 0) { - runOnUiThread(() -> showToast(getString(R.string.settings_synchronize_successful, filesCount))); + if (!syncResult.isSessionValid()) { + runOnUiThread(() -> showToast(getString(R.string.synchronize_session_not_valid))); + } + else if (!syncResult.isSessionAuthorized()) { + runOnUiThread(() -> showToast(getString(R.string.synchronize_session_not_authorized))); + } + else if (syncResult.isSyncFailed()) { + runOnUiThread(() -> showToast(getString(R.string.synchronize_failed))); + } + else if (syncResult.getFileCount() > 0) { + runOnUiThread(() -> showToast(getString(R.string.synchronize_successful, syncResult.getFileCount()))); WikiEngineApplication app = (WikiEngineApplication) getApplication(); app.resetServices(); CalendarSyncAdapter.requestCalendarSync(this); } else { - runOnUiThread(() -> showToast(getString(R.string.settings_synchronize_not_necessary))); + runOnUiThread(() -> showToast(getString(R.string.synchronize_not_necessary))); } runOnUiThread(this::hideProgressBar); diff --git a/app/src/main/java/net/moasdawiki/app/SettingsFragment.java b/app/src/main/java/net/moasdawiki/app/SettingsFragment.java index 7dfa1d1..0ebb671 100644 --- a/app/src/main/java/net/moasdawiki/app/SettingsFragment.java +++ b/app/src/main/java/net/moasdawiki/app/SettingsFragment.java @@ -185,10 +185,10 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Shared return; } - boolean success = synchronizeWikiClient.createAndCheckSession(); + SynchronizeWikiClient.SessionStatus sessionStatus = synchronizeWikiClient.createAndCheckSession(); updateStatusText(); - if (!success) { + if (!sessionStatus.isValid()) { showToast(getString(R.string.settings_search_failed)); } } diff --git a/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java b/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java index 99c6499..3a065f0 100644 --- a/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java +++ b/app/src/main/java/net/moasdawiki/app/SynchronizeWikiClient.java @@ -39,6 +39,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; @@ -47,14 +48,14 @@ import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.URI; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Date; import java.util.Enumeration; /** - * Sucht einen Wikiserver in Netzwerk und synchronisiert alle Wikidateien - * im eigenen Repository.. + * Connects to the configured MoasdaWiki server and downloads the wiki files. */ public class SynchronizeWikiClient { @@ -62,6 +63,10 @@ public class SynchronizeWikiClient { private static final String PROTOCOL_VERSION = "2.0"; + private static final int CONNECTION_CONNECT_TIMEOUT = 2_000; // 2 seconds + private static final int CONNECTION_READ_TIMEOUT = 120_000; // 2 minutes + private static final int CONNECTION_RETRIES = 3; + @NotNull private final Context mContext; @NotNull @@ -83,13 +88,13 @@ public class SynchronizeWikiClient { } /** - * Verbindet sich mit dem Wikiserver. Ist bereits eine Serversession vorhanden, wird diese - * weiter verwendet. + * Connects with the MoasdaWiki server. + * If there is already a valid server session, it is reused. */ - public boolean createAndCheckSession() { + public SessionStatus createAndCheckSession() { String serverHostPort = getServerHostPort(); if (serverHostPort == null) { - return false; + return new SessionStatus(false, false); } PreferenceManager.getDefaultSharedPreferences(mContext); @@ -97,37 +102,39 @@ public class SynchronizeWikiClient { String serverSessionId = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID, null); try { - // Wenn noch keine Session vorhanden ist, eine erzeugen + // If there is no session yet, create a new session boolean createSessionCalled = false; if (serverSessionId == null) { createSessionCalled = true; createSession(serverHostPort); } - // Ist Session freigeschaltet zur Synchronisierung? - boolean[] checkResult = checkSession(serverHostPort); - if (checkResult[0]) { - return true; + // Check existing session if it's still valid + SessionStatus sessionStatus = checkSession(serverHostPort); + if (sessionStatus.isValid()) { + return sessionStatus; } - // Session ist ungültig --> neue Session holen + // If session ist invalid, create new session if (createSessionCalled) { - // createSession nicht erneut aufrufen - return false; + // don't call createSession() twice + return new SessionStatus(false, false); } createSession(serverHostPort); - // Session erneut prüfen - boolean[] checkResult2 = checkSession(serverHostPort); - return checkResult2[0]; + // Check session again + return checkSession(serverHostPort); } catch (ServiceException e) { - return false; + return new SessionStatus(false, false); } } + /** + * Connects with the MoasdaWiki server and creates a new session. + */ private void createSession(@NotNull String serverHostPort) throws ServiceException { - // Anfrage schicken + // send request CreateSessionXml createSessionXml = new CreateSessionXml(); createSessionXml.version = PROTOCOL_VERSION; createSessionXml.clientSessionId = generateSessionId(); @@ -141,11 +148,11 @@ public class SynchronizeWikiClient { String requestXml = generateXml(createSessionXml); String responseXml = sendXmlRequest(serverHostPort, "/sync/create-session", requestXml); - // Antwort auswerten + // parse response CreateSessionResponseXml response = parseXml(responseXml, CreateSessionResponseXml.class); Log.d(TAG, "Current sync session ID '" + response.serverSessionId + "'"); - // Session prüfen + // check for session id if (response.serverSessionId == null) { throw new ServiceException("Didn't get a server session ID"); } @@ -157,29 +164,34 @@ public class SynchronizeWikiClient { editor.putString(Constants.PREFERENCES_SYNC_SERVER_HOST_DISPLAYNAME, response.serverHost); editor.putString(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID, response.serverSessionId); editor.putString(Constants.PREFERENCES_SYNC_CLIENT_SESSION_ID, createSessionXml.clientSessionId); - // Synchronisierungszeiten können nicht mehr genutzt werden + // reset sync data editor.remove(Constants.PREFERENCES_SYNC_SERVER_TIME); editor.remove(Constants.PREFERENCES_SYNC_SERVER_SESSION_AUTHORIZED); editor.apply(); } + @NotNull private String generateSessionId() { return new BigInteger(130, random).toString(32); } - private boolean[] checkSession(@NotNull String serverHostPort) throws ServiceException { + /** + * Check if our MoasdaWiki server session is valid and authorized. + */ + @NotNull + private SessionStatus checkSession(@NotNull String serverHostPort) throws ServiceException { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); String serverSessionId = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID, null); String clientSessionId = preferences.getString(Constants.PREFERENCES_SYNC_CLIENT_SESSION_ID, null); - // Anfrage schicken + // Send request CheckSessionXml checkSessionXml = new CheckSessionXml(); checkSessionXml.version = PROTOCOL_VERSION; checkSessionXml.serverSessionId = serverSessionId; String requestXml = generateXml(checkSessionXml); String responseXml = sendXmlRequest(serverHostPort, "/sync/check-session", requestXml); - // Antwort auswerten + // Parse response CheckSessionResponseXml response = parseXml(responseXml, CheckSessionResponseXml.class); if (response.valid == null || !response.valid) { Log.d(TAG, "Sync server session ID '" + serverSessionId + "' is not valid any more"); @@ -187,7 +199,7 @@ public class SynchronizeWikiClient { editor.remove(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID); editor.remove(Constants.PREFERENCES_SYNC_CLIENT_SESSION_ID); editor.apply(); - return new boolean[]{ false, false }; + return new SessionStatus(false, false); } if (clientSessionId != null && !clientSessionId.equals(response.clientSessionId)) { Log.d(TAG, "Sync server authentication failed, client session ID does not match"); @@ -195,37 +207,70 @@ public class SynchronizeWikiClient { editor.remove(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID); editor.remove(Constants.PREFERENCES_SYNC_CLIENT_SESSION_ID); editor.apply(); - return new boolean[]{ false, false }; + return new SessionStatus(false, false); } SharedPreferences.Editor editor = preferences.edit(); boolean authorized = response.authorized != null && response.authorized; editor.putBoolean(Constants.PREFERENCES_SYNC_SERVER_SESSION_AUTHORIZED, authorized); editor.apply(); - return new boolean[]{ true, authorized }; + return new SessionStatus(true, authorized); } /** - * Synchronisiert alle Repository-Dateien mit dem Wikiserver. Vorher wird geprüft, ob - * die Server-Session noch gültig ist und wir mit demselben Server verbunden sind. - * - * @return Anzahl der synchronisierten Dateien. + * Contains the session status. */ - public int synchronizeRepository(ProgressFeedback feedback) throws ServiceException { - boolean sessionValid = createAndCheckSession(); - if (!sessionValid) { - throw new ServiceException("No valid server session found"); + public static class SessionStatus { + /** + * Is the MoasdaWiki server session valid? + */ + private final boolean valid; + + /** + * Is the session authorized at server side? + */ + private final boolean authorized; + + public SessionStatus(boolean valid, boolean authorized) { + this.valid = valid; + this.authorized = authorized; } + public boolean isValid() { + return valid; + } + + public boolean isAuthorized() { + return authorized; + } + } + + /** + * Downloads the wiki files from the MoasdaWiki server. + * Checks if the session is still valid. + */ + public SyncResult synchronizeRepository(ProgressFeedback feedback) throws ServiceException { String serverHostPort = getServerHostPort(); if (serverHostPort == null) { - throw new ServiceException("No wiki server configured"); + Log.w(TAG, "No server host name configured"); + return new SyncResult(false, false, true, 0); + } + + SessionStatus sessionStatus = createAndCheckSession(); + if (!sessionStatus.isValid()) { + Log.w(TAG, "No valid server session"); + return new SyncResult(false, false, true, 0); + } + if (!sessionStatus.isAuthorized()) { + Log.w(TAG, "Server session not authorized"); + return new SyncResult(true, false, true, 0); } SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); String serverSessionId = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID, null); if (serverSessionId == null) { - throw new ServiceException("Not server session ID found"); + Log.w(TAG, "No server session available"); + return new SyncResult(false, false, true, 0); } long lastSyncServerTimeMs = preferences.getLong(Constants.PREFERENCES_SYNC_SERVER_TIME, 0); Date lastSyncServerTime = null; @@ -247,7 +292,7 @@ public class SynchronizeWikiClient { Log.d(TAG, "Downloading " + fileCount + " files from server"); if (fileCount == 0) { // no files to download, cancel process - return 0; + return new SyncResult(true, true, false, 0); } for (int i = 0; i < fileCount; i++) { @@ -274,7 +319,37 @@ public class SynchronizeWikiClient { editor.apply(); } - return fileCount; + return new SyncResult(true, true, false, fileCount); + } + + public static class SyncResult { + private final boolean sessionValid; + private final boolean sessionAuthorized; + private final boolean syncFailed; + private final int fileCount; + + public SyncResult(boolean sessionValid, boolean sessionAuthorized, boolean syncFailed, int fileCount) { + this.sessionValid = sessionValid; + this.sessionAuthorized = sessionAuthorized; + this.syncFailed = syncFailed; + this.fileCount = fileCount; + } + + public boolean isSessionValid() { + return sessionValid; + } + + public boolean isSessionAuthorized() { + return sessionAuthorized; + } + + public boolean isSyncFailed() { + return syncFailed; + } + + public int getFileCount() { + return fileCount; + } } private void downloadFileFromServer(@NotNull String serverHostPort, @NotNull String serverSessionId, @NotNull String filePath) throws ServiceException { @@ -372,11 +447,7 @@ public class SynchronizeWikiClient { } /** - * Schickt einen XML-Request und liest die XML-Antwort ein. - * - * @param urlPath HTTP-URL, nicht null - * @param requestXml XML-Anfrage, nicht null. - * @return XML-Antwort, nicht null. + * Sends an XML request and reads the XML response. */ @NotNull private String sendXmlRequest(@NotNull String serverHostPort, @NotNull String urlPath, @NotNull String requestXml) throws ServiceException { @@ -385,32 +456,11 @@ public class SynchronizeWikiClient { Log.d(TAG, "Request to " + url + ": " + truncateLogText(requestXml, 200)); byte[] requestBytes = requestXml.getBytes(StandardCharsets.UTF_8); - URI uri = new URI(url); - HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection(); - conn.setRequestMethod("POST"); - conn.setRequestProperty("Content-Type", "text/xml"); - conn.setRequestProperty("Content-Length", Integer.toString(requestBytes.length)); - conn.setUseCaches(false); - conn.setDoOutput(true); - conn.setDoInput(true); - conn.setConnectTimeout(1000); // 1 Sekunde - conn.setReadTimeout(10000); // 10 Sekunden - conn.connect(); + byte[] responseBytes = sendBinaryRequestWithRetries(new URI(url).toURL(), requestBytes); - OutputStream out = conn.getOutputStream(); - out.write(requestBytes); - out.flush(); - - InputStream in = conn.getInputStream(); - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(in.available()); - int bytesRead; - byte[] buffer = new byte[1024]; - while ((bytesRead = in.read(buffer)) != -1) { - byteStream.write(buffer, 0, bytesRead); - } - - String responseXml = byteStream.toString("UTF-8"); - Log.d(TAG, "Response: " + truncateLogText(responseXml, 400)); + //noinspection CharsetObjectCanBeUsed + String responseXml = new String(responseBytes, "UTF-8"); + Log.d(TAG, "Response: " + truncateLogText(responseXml, 100)); return responseXml; } catch (Exception e) { Log.e(TAG, "Error sending XML request", e); @@ -418,6 +468,44 @@ public class SynchronizeWikiClient { } } + private byte[] sendBinaryRequestWithRetries(@NotNull URL url, byte[] requestBytes) throws ServiceException { + for (int i = 1; i <= CONNECTION_RETRIES; i++) { + try { + return sendBinaryRequest(url, requestBytes); + } + catch (Exception e) { + Log.d(TAG, "Error sending request to MoasdaWiki server in attempt " + i + " of " + CONNECTION_RETRIES, e); + } + } + throw new ServiceException("Error sending request to MoasdaWiki server for " + CONNECTION_RETRIES + " times, failed"); + } + + private byte[] sendBinaryRequest(@NotNull URL url, byte[] requestBytes) throws IOException { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "text/xml"); + conn.setRequestProperty("Content-Length", Integer.toString(requestBytes.length)); + conn.setUseCaches(false); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setConnectTimeout(CONNECTION_CONNECT_TIMEOUT); + conn.setReadTimeout(CONNECTION_READ_TIMEOUT); + conn.connect(); + + OutputStream out = conn.getOutputStream(); + out.write(requestBytes); + out.flush(); + + InputStream in = conn.getInputStream(); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(in.available()); + int bytesRead; + byte[] buffer = new byte[1024]; + while ((bytesRead = in.read(buffer)) != -1) { + byteStream.write(buffer, 0, bytesRead); + } + return byteStream.toByteArray(); + } + private String truncateLogText(String logText, int maxLength) { if (logText.length() <= maxLength) { return logText; @@ -443,7 +531,7 @@ public class SynchronizeWikiClient { XmlParser xmlParser = new XmlParser(logger); return xmlParser.parse(xml, xmlBeanType); } catch (ServiceException e) { - logger.write("Failed to parse XML for class " + xmlBeanType.getSimpleName() + ", try class ErrorResponseXml", e); + Log.d(TAG, "Failed to parse XML for class " + xmlBeanType.getSimpleName() + ", try class ErrorResponseXml", e); // Versuche eine Fehlerantwort zu parsen XmlParser xmlParser = new XmlParser(logger); ErrorResponseXml errorResponseXml = xmlParser.parse(xml, ErrorResponseXml.class); diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 64e1c3c..3c99a58 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -28,7 +28,9 @@ Am Server angemeldet Erfordert Berechtigung am Server Keinen Wikiserver gefunden! - %1$d Dateien erfolgreich synchronisiert - Dateien sind bereits aktuell - Synchronisierung nicht möglich, bitte überprüfen Sie die Einstellungen und den Status! + %1$d Dateien erfolgreich synchronisiert + Dateien sind bereits aktuell + Keine Verbindung zum MoasdaWiki-Server vorhanden! Bitte überprüfe die Einstellungen und den Status! + Fehlende Berechtigung beim MoasdaWiki-Server! Bitte schalte den Zugang für die App beim Server frei! + Synchronisierung nicht möglich, bitte überprüfe die Einstellungen und den Status! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57ae670..4ddebe9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,7 +29,9 @@ Connected to server Needs authorization at server No Wiki server found! - Successfully synchronized %1$d files - All files are up-to-date - Synchronization failed, please check settings and status! + Successfully synchronized %1$d files + All files are up-to-date + No connection to MoasdaWiki server available! Please check settings and status! + Connection to MoasdaWiki server not authorized! Please authorize at server side first! + Synchronization failed, please check settings and status!