Initial check-in

This commit is contained in:
Herbert Reiter
2020-01-04 19:36:50 +01:00
parent 510a54d77b
commit 3f7f6cb028
65 changed files with 2547 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="3.5.3" />
<option name="LAST_KNOWN_AGP_VERSION" value="3.5.3" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<afterSyncTasks>
<task>generateDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
<option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/build/intermediates/javac/debug/classes" />
<output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/classes" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debug/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debugAndroidTest/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/ap_generated_sources/debugUnitTest/out" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Android API 29 Platform (2)" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Gradle: org.testng:testng:6.14.3@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: com.beust:jcommander:1.72@jar" level="project" />
<orderEntry type="library" scope="TEST" name="Gradle: org.apache-extras.beanshell:bsh:2.0b6@jar" level="project" />
<orderEntry type="library" name="Gradle: __local_aars__:/home/herbert/Schreibtisch/moasdawiki-server-2.1.0.jar:unspecified@jar" level="project" />
<orderEntry type="library" name="Gradle: org.jetbrains:annotations:18.0.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.collection:collection:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-common:2.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.arch.core:core-common:2.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.annotation:annotation:1.1.0@jar" level="project" />
<orderEntry type="library" name="Gradle: androidx.preference:preference:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.fragment:fragment:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat-resources:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.recyclerview:recyclerview:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.legacy:legacy-support-core-ui:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.drawerlayout:drawerlayout:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.viewpager:viewpager:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.legacy:legacy-support-core-utils:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.loader:loader:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.activity:activity:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable-animated:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.coordinatorlayout:coordinatorlayout:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.slidingpanelayout:slidingpanelayout:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.customview:customview:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.swiperefreshlayout:swiperefreshlayout:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.asynclayoutinflater:asynclayoutinflater:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.core:core:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.cursoradapter:cursoradapter:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-viewmodel:2.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-runtime:2.1.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.savedstate:savedstate:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata-core:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.interpolator:interpolator:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.arch.core:core-runtime:2.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.documentfile:documentfile:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.localbroadcastmanager:localbroadcastmanager:1.0.0@aar" level="project" />
<orderEntry type="library" name="Gradle: androidx.print:print:1.0.0@aar" level="project" />
</component>
</module>
+69
View File
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.moasdawiki.app">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<application
android:icon="@mipmap/ic_cow"
android:label="@string/app_name"
android:name=".WikiEngineApplication"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:allowBackup="false">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/settings_title"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="net.moasdawiki.app.MainActivity" />
</activity>
<provider
android:name="net.moasdawiki.app.CalendarContentProvider"
android:authorities="net.moasdawiki.app.provider"
android:exported="false"
android:syncable="true"/>
<service
android:name="net.moasdawiki.app.CalendarAccountAuthenticatorService"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name="net.moasdawiki.app.CalendarSyncAdapterService"
android:exported="true"
android:process=":sync"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
</application>
</manifest>
@@ -0,0 +1,37 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import net.moasdawiki.base.Logger;
import net.moasdawiki.base.Settings;
import net.moasdawiki.service.repository.RepositoryService;
import org.jetbrains.annotations.NotNull;
public class AndroidSettings extends Settings {
public AndroidSettings(@NotNull Logger logger, @NotNull RepositoryService repositoryService, @NotNull String configFileName) {
super(logger, repositoryService, configFileName);
}
@NotNull
public String getVersion() {
return BuildConfig.VERSION_NAME;
}
}
@@ -0,0 +1,83 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.content.Context;
import android.os.Bundle;
import org.jetbrains.annotations.Nullable;
/**
* Stub authenticator, required for calendar sync adapter.
*
* @see "https://developer.android.com/training/sync-adapters/creating-authenticator#CreateAuthenticator"
*/
public class CalendarAccountAuthenticator extends AbstractAccountAuthenticator {
public CalendarAccountAuthenticator(Context context) {
super(context);
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
// Editing properties is not supported
throw new UnsupportedOperationException();
}
@Nullable
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) {
// Don't add additional accounts
return null;
}
@Nullable
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) {
// Ignore attempts to confirm credentials
return null;
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) {
// Getting an authentication token is not supported
throw new UnsupportedOperationException();
}
@Override
public String getAuthTokenLabel(String authTokenType) {
// Getting a label for the auth token is not supported
throw new UnsupportedOperationException();
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) {
// Updating user credentials is not supported
throw new UnsupportedOperationException();
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) {
// Checking features for the account is not supported
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,45 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import org.jetbrains.annotations.Nullable;
/**
* Service to bind the CalendarAccountAuthenticator.
*/
public class CalendarAccountAuthenticatorService extends Service {
private CalendarAccountAuthenticator authenticator;
@Override
public void onCreate() {
// Create a new authenticator object
authenticator = new CalendarAccountAuthenticator(this);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return authenticator.getIBinder();
}
}
@@ -0,0 +1,70 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Stub content provider, necessary for sync adapter.
*/
public class CalendarContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return true;
}
@Nullable
@Override
public String getType(@NotNull Uri uri) {
// Return no type for MIME type
return null;
}
@Nullable
@Override
public Cursor query(@NotNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
// query() always returns no results
return null;
}
@Nullable
@Override
public Uri insert(@NotNull Uri uri, @Nullable ContentValues contentValues) {
// Provider doesn't support changes from outside
return null;
}
@Override
public int delete(@NotNull Uri uri, @Nullable String s, @Nullable String[] strings) {
// Provider doesn't support changes from outside
return 0;
}
@Override
public int update(@NotNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
// Provider doesn't support changes from outside
return 0;
}
}
@@ -0,0 +1,323 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.CalendarContract;
import androidx.core.app.ActivityCompat;
import androidx.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
import net.moasdawiki.plugin.Plugin;
import net.moasdawiki.plugin.PluginService;
import net.moasdawiki.plugin.TerminPlugin;
import net.moasdawiki.util.PathUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;
import static android.content.Context.ACCOUNT_SERVICE;
/**
* Creates an Android calendar with all events found on Wiki pages and keeps them up-to-date.
*/
public class CalendarSyncAdapter extends AbstractThreadedSyncAdapter {
private static final String TAG = "CalendarSyncAdapter";
static final String ACCOUNT_NAME = "MoasdaWiki";
static final String ACCOUNT_TYPE = "net.moasdawiki";
static final String PROVIDER_NAME = "net.moasdawiki.app.provider";
private static final String CALENDAR_NAME = "MoasdaWiki Events";
private static final Uri CALENDAR_URI = CalendarContract.Calendars.CONTENT_URI;
private static final Uri EVENT_URI = CalendarContract.Events. CONTENT_URI;
private static final Uri REMINDER_URI = CalendarContract.Reminders.CONTENT_URI;
private final ContentResolver contentResolver;
@SuppressWarnings("SameParameterValue")
CalendarSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
contentResolver = context.getContentResolver();
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.d(TAG, "Begin of onPerformSync()");
List<TerminPlugin.Event> events = getWikiEvents();
String calendarId = createCalendar();
if (calendarId != null) {
deleteAllEvents(calendarId);
addEvents(calendarId, events);
}
Handler handler = new Handler(Looper.getMainLooper());
handler.post(() -> {
Toast toast = Toast.makeText(getContext(), R.string.calendar_sync_finished, Toast.LENGTH_SHORT);
toast.show();
});
Log.d(TAG, "End of onPerformSync()");
}
/**
* Imports all events on Wiki pages to the Android calendar.
*/
@NotNull
private List<TerminPlugin.Event> getWikiEvents() {
Log.d(TAG, "Reading Wiki events");
WikiEngineApplication app = (WikiEngineApplication) getContext();
PluginService pluginService = app.getServiceLocator().getPluginService();
TerminPlugin terminPlugin = null;
for (Plugin plugin : pluginService.getPlugins()) {
if (plugin instanceof TerminPlugin) {
terminPlugin = (TerminPlugin) plugin;
}
}
if (terminPlugin == null) {
Log.e(TAG, "TerminPlugin not found, cannot retrieve event list");
return Collections.emptyList();
}
List<TerminPlugin.Event> events = terminPlugin.getEvents();
Log.d(TAG, "Wiki events found: " + events.size());
return events;
}
@NotNull
private Uri buildUri(@NotNull Uri uri) {
return uri.buildUpon()
.appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, ACCOUNT_NAME)
.appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, ACCOUNT_TYPE).build();
}
/**
* Returns the ID of the event calendar. If the calendar doesn't exist, it is created.
*
* @return Calendar ID
*/
@Nullable
private String createCalendar() {
// Calendar already existing?
Log.d(TAG, "Check if calendar already exists");
Uri calendarUri = buildUri(CALENDAR_URI);
try (Cursor cursor = contentResolver.query(calendarUri,
new String[] { CalendarContract.Calendars._ID },
"(" + CalendarContract.Calendars.ACCOUNT_TYPE + " = ? AND " + CalendarContract.Calendars.NAME + " = ?)",
new String[] { ACCOUNT_TYPE, CALENDAR_NAME },
null)) {
if (cursor != null && cursor.moveToFirst()) {
// Calendar is already existing
String calendarId = cursor.getString(0);
Log.d(TAG, "Calendar already exists, id=" + calendarId);
return calendarId;
}
}
// Create a new calendar
Log.d(TAG, "Calendar does not exist, create new one");
ContentValues cv = new ContentValues();
cv.put(CalendarContract.Calendars.ACCOUNT_NAME, ACCOUNT_NAME);
cv.put(CalendarContract.Calendars.ACCOUNT_TYPE, ACCOUNT_TYPE);
cv.put(CalendarContract.Calendars.NAME, CALENDAR_NAME);
String displayName = getContext().getString(R.string.calendar_display_name);
cv.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName);
//cv.put(CalendarContract.Calendars.CALENDAR_COLOR, 0xEA8561);
cv.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_READ);
cv.put(CalendarContract.Calendars.OWNER_ACCOUNT, ACCOUNT_NAME);
cv.put(CalendarContract.Calendars.VISIBLE, 1);
cv.put(CalendarContract.Calendars.SYNC_EVENTS, 1);
Uri newCalendar = contentResolver.insert(calendarUri, cv);
Log.i(TAG, "Created calendar: " + newCalendar);
if (newCalendar != null) {
return newCalendar.getLastPathSegment();
} else {
return null;
}
}
/**
* Clears the calendar. This is necessary if e.g. a birthday has been changed or removed.
*/
private void deleteAllEvents(@NotNull String calendarId) {
Log.d(TAG, "Delete all events from calendar");
Uri eventUri = buildUri(EVENT_URI);
try (Cursor cursor = contentResolver.query(eventUri,
new String[] { CalendarContract.Events._ID },
"(" + CalendarContract.Events.CALENDAR_ID + " = ?)",
new String[] { calendarId },
null)) {
int count = 0;
while (cursor != null && cursor.moveToNext()) {
Uri deleteUri = ContentUris.withAppendedId(eventUri, cursor.getInt(0));
contentResolver.delete(deleteUri, null, null);
count++;
}
Log.d(TAG, "Deleted " + count + " events from calendar");
}
}
/**
* Adds all events from the event list. The first occurrence is in the current year, the events
* are repeated every year.
*/
private void addEvents(@NotNull String calendarId, @NotNull List<TerminPlugin.Event> events) {
Log.d(TAG, "Create calendar events");
for (TerminPlugin.Event event : events) {
int day = event.dateFields.day != null ? event.dateFields.day : 1;
int month = event.dateFields.month != null ? event.dateFields.month : 1; // 1=January
String title = event.description;
if (title == null) {
title = PathUtils.extractWebName(event.pagePath);
}
String description = getContext().getString(R.string.calendar_date) + ": " + TerminPlugin.formatGermanDate(event.dateFields);
String eventId = addEvent(calendarId, day, month, title, description);
if (eventId != null) {
addReminder(eventId);
}
}
Log.i(TAG, "Created events: " + events.size());
}
/**
* Adds a single event to the calendar.
*/
@Nullable
private String addEvent(@NotNull String calendarId, int day, int month, @NotNull String title, @NotNull String description) {
Log.d(TAG, "Create calendar event: day=" + day + ", month=" + month
+ ", title=" + title + ", description=" + description);
ContentValues cv = new ContentValues();
cv.put(CalendarContract.Events.CALENDAR_ID, calendarId);
cv.put(CalendarContract.Events.TITLE, title);
cv.put(CalendarContract.Events.DESCRIPTION, description);
TimeZone utc = TimeZone.getTimeZone("UTC");
Calendar beginTime = Calendar.getInstance(utc);
int currentYear = beginTime.get(Calendar.YEAR);
beginTime.clear();
beginTime.set(currentYear, month - 1, day);
cv.put(CalendarContract.Events.DTSTART, beginTime.getTimeInMillis());
cv.put(CalendarContract.Events.DURATION, "PT1D");
cv.put(CalendarContract.Events.ALL_DAY, 1);
cv.put(CalendarContract.Events.RRULE, "FREQ=YEARLY");
cv.put(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_FREE);
Uri eventUri = buildUri(EVENT_URI);
Uri newEvent = contentResolver.insert(eventUri, cv);
Log.i(TAG, "Created event: " + newEvent);
if (newEvent != null) {
return newEvent.getLastPathSegment();
} else {
return null;
}
}
/**
* Add a reminder to the given event.
*/
private void addReminder(@NotNull String eventId) {
Log.d(TAG, "Add reminder to eventId=" + eventId);
ContentValues cv = new ContentValues();
cv.put(CalendarContract.Reminders.EVENT_ID, eventId);
cv.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT);
cv.put(CalendarContract.Reminders.MINUTES, 0); // 0 minutes offset
Uri reminderUri = buildUri(REMINDER_URI);
Uri newReminder = contentResolver.insert(reminderUri, cv);
Log.d(TAG, "Created reminder: " + newReminder);
}
/**
* Initiates the calendar sync.
*/
public static void requestCalendarSync(@NotNull Activity activity) {
Log.d(TAG, "Requesting calendar synchronization");
Context context = activity.getApplicationContext();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean calendarEnabled = preferences.getBoolean(Constants.PREFERENCES_CALENDAR_ENABLED, false);
if (!calendarEnabled) {
Log.d(TAG, "Calendar integration disabled, cancel operation");
return;
}
try {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_CALENDAR)) {
Log.d(TAG, "User has permanently denied permission WRITE_CALENDAR, informing him");
String hint = context.getString(R.string.calendar_permission_request, "WRITE_CALENDAR");
Toast toast = Toast.makeText(context, hint, Toast.LENGTH_SHORT);
toast.show();
}
Log.d(TAG, "Ask for permission WRITE_CALENDAR");
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_CALENDAR}, 0);
return;
}
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.READ_CALENDAR)) {
Log.d(TAG, "User has permanently denied permission READ_CALENDAR, informing him");
String hint = context.getString(R.string.calendar_permission_request, "READ_CALENDAR");
Toast toast = Toast.makeText(context, hint, Toast.LENGTH_SHORT);
toast.show();
}
Log.d(TAG, "Ask for permission READ_CALENDAR");
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_CALENDAR}, 0);
return;
}
Account syncAccount = new Account(CalendarSyncAdapter.ACCOUNT_NAME, CalendarSyncAdapter.ACCOUNT_TYPE);
AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE);
if (accountManager != null && !accountManager.addAccountExplicitly(syncAccount, null, null)) {
Log.e(TAG, "Error creating sync account, maybe account already exists");
}
Bundle settingsBundle = new Bundle();
settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
ContentResolver.requestSync(syncAccount, CalendarSyncAdapter.PROVIDER_NAME, settingsBundle);
Log.d(TAG, "syncCalendar() finished");
}
catch (Throwable t) {
Log.e(TAG, "Error in syncCalendar()", t);
}
}
}
@@ -0,0 +1,46 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import org.jetbrains.annotations.Nullable;
public class CalendarSyncAdapterService extends Service {
private static CalendarSyncAdapter syncAdapter = null;
private static final Object syncAdapterLock = new Object();
@Override
public void onCreate() {
synchronized (syncAdapterLock) {
if (syncAdapter == null) {
syncAdapter = new CalendarSyncAdapter(getApplicationContext(), true, false);
}
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
}
}
@@ -0,0 +1,37 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
/**
* Enthält zentrale Konstanten.
*
* @author Herbert Reiter
*/
public abstract class Constants {
public static final String PREFERENCES_SYNC_SERVER_HOST = "sync_server_host";
public static final String PREFERENCES_SYNC_SERVER_PORT = "sync_server_port";
public static final String PREFERENCES_SYNC_SERVER_NAME = "sync_server_name";
public static final String PREFERENCES_SYNC_SERVER_VERSION = "sync_server_version";
public static final String PREFERENCES_SYNC_SERVER_HOST_DISPLAYNAME = "sync_server_host_displayname";
public static final String PREFERENCES_SYNC_SERVER_SESSION_ID = "sync_server_session_id";
public static final String PREFERENCES_SYNC_CLIENT_SESSION_ID = "sync_client_session_id";
public static final String PREFERENCES_SYNC_SERVER_SESSION_AUTHORIZED = "sync_server_session_authorized";
public static final String PREFERENCES_SYNC_SERVER_TIME = "last_sync_server_time";
public static final String PREFERENCES_CALENDAR_ENABLED = "calendar_enabled";
}
@@ -0,0 +1,510 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.CookieManager;
import android.webkit.WebBackForwardList;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.menu.MenuBuilder;
import androidx.preference.PreferenceManager;
import net.moasdawiki.base.ServiceException;
import net.moasdawiki.base.Settings;
import net.moasdawiki.plugin.Plugin;
import net.moasdawiki.plugin.PluginService;
import net.moasdawiki.server.HttpRequest;
import net.moasdawiki.server.HttpResponse;
import net.moasdawiki.service.render.HtmlService;
import net.moasdawiki.service.repository.RepositoryService;
import net.moasdawiki.util.EscapeUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Steuert das Verhalten des Hauptfensters inkl. eingebettetem Browser.
*
* @author Herbert Reiter
*/
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final String SERVER_BASE_URL = "http://localhost:1/";
private Settings settings;
private RepositoryService repositoryService;
private PluginService pluginService;
private HtmlService htmlService;
private SynchronizeWikiClient synchronizeWikiClient;
private WebView webview;
private long backButtonPressedTimestamp;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout);
// AndroidMainService holen
WikiEngineApplication app = (WikiEngineApplication) getApplication();
settings = app.getServiceLocator().getSettings();
repositoryService = app.getServiceLocator().getRepositoryService();
pluginService = app.getServiceLocator().getPluginService();
htmlService = app.getServiceLocator().getHtmlService();
synchronizeWikiClient = app.getSynchronizeWikiClient();
// eingebetteten Browser konfigurieren
initWebView();
EditText searchInput = findViewById(R.id.search_input);
searchInput.setOnEditorActionListener((TextView v, int actionId, KeyEvent event) -> {
// event==null for on screen keyboard
// filter for key up from physical keyboard
if (event == null || event.getAction() == KeyEvent.ACTION_DOWN) {
onSearch(null);
return true;
} else {
return false;
}
});
}
/**
* Konfiguriert den eingebetteten Browser.
*/
@SuppressLint("SetJavaScriptEnabled")
private void initWebView() {
// Cookies deaktivieren
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(false);
// localhost-URLs in WebView anzeigen
webview = findViewById(R.id.web_browser);
webview.setWebChromeClient(new WebChromeClient());
webview.setWebViewClient(new WebViewClient() {
/**
* Steuert, welche Links im eingebetteten Browser und welche im externen Browser
* geöffnet werden sollen.
*/
@Override
public boolean shouldOverrideUrlLoading(@NotNull WebView view, @NotNull WebResourceRequest webResourceRequest) {
Uri uri = webResourceRequest.getUrl();
String host = uri.getHost();
if ("localhost".equals(host)) {
// lokale URL im eingebetteten Browser öffnen
return false;
} else {
// externe Links im normalen Browser öffnen
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
return true;
}
}
/**
* Emuliert den Wikiserver und zu vermeiden, dass ein TCP-Port geöffnet werden muss.
*/
@Override
@Nullable
public WebResourceResponse shouldInterceptRequest (@NotNull WebView view, @NotNull WebResourceRequest request) {
try {
// URL-Pfad ermitteln
Uri uri = request.getUrl();
String encodedUrl = uri.toString();
String url = URLDecoder.decode(encodedUrl, "UTF-8");
if (!url.startsWith(SERVER_BASE_URL)) {
return null; // Daten per HTTP laden
}
String urlPath = url.substring(SERVER_BASE_URL.length() - 1); // ersten "/" behalten
int hashPos = urlPath.indexOf('#');
if (hashPos >= 0) {
// cut off anchor beginning with '#'
urlPath = urlPath.substring(0, hashPos);
}
// per URL-Mapping das zuständige Plugin aufrufen
HttpResponse response;
Plugin plugin = pluginService.getPluginByUrl(urlPath);
if (plugin != null) {
HttpRequest httpRequest = new HttpRequest();
httpRequest.clientIP = InetAddress.getLocalHost();
httpRequest.httpHeader = Collections.emptyMap();
httpRequest.method = "GET";
httpRequest.url = urlPath;
httpRequest.urlPath = urlPath;
httpRequest.urlParameters = convertParameters(uri);
httpRequest.httpBody = new byte[0];
response = plugin.handleRequest(httpRequest);
if (response == null) {
response = htmlService.generateErrorPage(404, "wiki.plugin.handleRequest.notsupported", plugin.getClass().getName());
}
} else {
// unbekannte URL
response = htmlService.generateErrorPage(404, "wiki.server.url.unmapped", urlPath);
}
// Antwortdaten einspeisen
InputStream responseData = new ByteArrayInputStream(response.getContent());
return new WebResourceResponse(response.getContentType(),
"UTF-8", responseData);
} catch (IOException e) {
e.printStackTrace();
}
return null; // Daten per HTTP laden
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
hideProgressBar();
}
});
// weitere Einstellungen
WebSettings webSettings = webview.getSettings();
webSettings.setAllowFileAccess(false);
webSettings.setJavaScriptEnabled(true);
}
@NotNull
private Map<String, String> convertParameters(@NotNull Uri uri) {
Map<String, String> result = new HashMap<>();
for (String name : uri.getQueryParameterNames()) {
String value = uri.getQueryParameter(name);
result.put(name, value);
}
return result;
}
@Override
public boolean onCreateOptionsMenu(@NotNull Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_layout, menu);
if(menu instanceof MenuBuilder){
((MenuBuilder) menu).setOptionalIconsVisible(true);
}
return true;
}
@Override
protected void onResume() {
super.onResume();
updateLayoutVisibility();
}
@Override
public boolean onOptionsItemSelected(@NotNull MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
switch (item.getItemId()) {
case R.id.action_synchronize:
synchronizeServer();
return true;
case R.id.action_startpage:
loadUrl(SERVER_BASE_URL);
return true;
case R.id.action_settings:
showSettingsDialog();
return true;
case R.id.action_help:
String pagePathHelp = getWikiserverHelpUrl();
loadUrl(pagePathHelp);
return true;
case R.id.action_about:
showAboutDialog();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) {
Log.d(TAG, "Permission granted by user: requestCode=" + requestCode
+ ", permissions=" + Arrays.toString(permissions) + ", grantResults=" + Arrays.toString(grantResults));
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public void onBackPressed() {
// webview.canGoBack() liefert immer false
WebBackForwardList backForwardList = webview.copyBackForwardList();
Log.d(TAG, "Back button pressed, backForwardList index == " + backForwardList.getCurrentIndex());
if (backForwardList.getCurrentIndex() > 0) {
webview.goBack();
} else {
long currentTimeMillis = System.currentTimeMillis();
if (backButtonPressedTimestamp + 5000 < currentTimeMillis) {
// First click on back button or previous click was more than 5 seconds ago
Log.d(TAG, "Back button 1x, show close hint");
showToast(getString(R.string.action_back_close_hint));
backButtonPressedTimestamp = currentTimeMillis;
} else {
// Second click on back button within 5 seconds -> close app
Log.d(TAG, "Back button 2x, closing app");
finish();
}
}
}
public void onConfigurationHintClicked(@SuppressWarnings("unused") View view) {
showSettingsDialog();
}
public void onSynchronizeHintClicked(@SuppressWarnings("unused") View view) {
synchronizeServer();
}
public void onSearch(@SuppressWarnings("unused") View view) {
EditText searchInput = findViewById(R.id.search_input);
String query = searchInput.getText().toString();
query = query.trim();
if (query.isEmpty()) {
return;
}
// Remove focus + on screen keyboard from input field
searchInput.clearFocus();
InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (manager != null) {
manager.hideSoftInputFromWindow(searchInput.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
Log.i(TAG, "Full text search for: " + query);
showProgressBarAnimation();
String url = getWikiserverSearchUrl(query);
loadUrl(url);
// Der Wartedialog wird nach dem Anzeigen der Suchergebnisse durch Aufruf von
// closeProgressDialog() geschlossen.
}
private void showSettingsDialog() {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
}
private void showAboutDialog() {
// Versionsnummer einsetzen
@SuppressLint("InflateParams")
View dialogView = getLayoutInflater().inflate(R.layout.about_layout, null);
TextView versionText = dialogView.findViewById(R.id.label_version);
versionText.setText(getString(R.string.about_version, settings.getVersion()));
int year = Calendar.getInstance().get(Calendar.YEAR);
TextView copyrightText = dialogView.findViewById(R.id.label_copyright);
copyrightText.setText(getString(R.string.about_copyright, year));
// Dialog anzeigen
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(dialogView);
builder.setTitle(R.string.app_name);
builder.setIcon(R.mipmap.ic_cow);
builder.setPositiveButton(R.string.button_close, (dialog, id) -> {});
Dialog dialog = builder.create();
dialog.show();
}
private void synchronizeServer() {
SyncNowTask syncNowTask = new SyncNowTask();
syncNowTask.execute();
}
/**
* Öffnet die angegebene URL im eingebetteten Browser.
*/
private void loadUrl(@NotNull String url) {
Log.d(TAG, "Open URL " + url);
webview.loadUrl(url);
}
@NotNull
private String getWikiserverSearchUrl(@NotNull String query) {
return SERVER_BASE_URL + "search/?text=" + EscapeUtils.encodeUrlParameter(query);
}
@NotNull
private String getWikiserverHelpUrl() {
return SERVER_BASE_URL + "view/wiki/";
}
private void updateLayoutVisibility() {
// Show/hide hint on unconfigured host name
LinearLayout hostUnconfiguredHint = findViewById(R.id.hint_host_unconfigured);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
String host = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST, null);
boolean hostUnconfiguredHintVisible = (host == null || host.isEmpty());
if (hostUnconfiguredHintVisible) {
hostUnconfiguredHint.setVisibility(View.VISIBLE);
} else {
hostUnconfiguredHint.setVisibility(View.GONE);
}
// Show/hide hint on synchronization
LinearLayout repositoryEmptyHint = findViewById(R.id.hint_repository_empty);
boolean repositoryEmptyHintVisible = (!hostUnconfiguredHintVisible && repositoryService.getFiles().size() < 3);
if (repositoryEmptyHintVisible) {
repositoryEmptyHint.setVisibility(View.VISIBLE);
} else {
repositoryEmptyHint.setVisibility(View.GONE);
}
// Show/hide search bar and wiki view
LinearLayout searchArea = findViewById(R.id.search_area);
WebView webView = findViewById(R.id.web_browser);
boolean webViewVisible = !hostUnconfiguredHintVisible && !repositoryEmptyHintVisible;
if (webViewVisible) {
searchArea.setVisibility(View.VISIBLE);
boolean previousVisible = (webView.getVisibility() == View.INVISIBLE);
webView.setVisibility(View.VISIBLE);
// if WebView is shown the first time, go to start page
if (!previousVisible) {
loadUrl(SERVER_BASE_URL);
}
} else {
searchArea.setVisibility(View.GONE);
webView.setVisibility(View.GONE);
}
}
private void showProgressBarAnimation() {
ProgressBar progressBar = findViewById(R.id.search_progressbar);
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
private void showProgressBar(int progress, int max) {
ProgressBar progressBar = findViewById(R.id.search_progressbar);
progressBar.setIndeterminate(false);
progressBar.setMin(0);
progressBar.setMax(max);
progressBar.setProgress(progress);
progressBar.setVisibility(View.VISIBLE);
}
private void hideProgressBar() {
ProgressBar progressBar = findViewById(R.id.search_progressbar);
progressBar.setVisibility(View.GONE);
}
private void showToast(String message) {
Toast toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
toast.show();
}
/**
* Synchronisiert mit dem Wikiserver in einem separaten Thread.
*/
@SuppressLint("StaticFieldLeak")
@SuppressWarnings("NonStaticInnerClassInSecureContext")
private class SyncNowTask extends AsyncTask<Void, ProgressData, Integer> implements SynchronizeWikiClient.ProgressFeedback {
@Nullable
@Override
protected Integer doInBackground(Void... params) {
try {
return synchronizeWikiClient.synchronizeRepository(this);
} catch (ServiceException e) {
Log.e(TAG, "Error synchronizing repository", e);
}
return null;
}
@Override
public void progress(int step, int total) {
ProgressData progressData = new ProgressData();
progressData.step = step;
progressData.total = total;
publishProgress(progressData);
}
@Override
protected void onProgressUpdate(ProgressData... progressData) {
showProgressBar(progressData[0].step, progressData[0].total);
}
@Override
protected void onPostExecute(@Nullable Integer filesCount) {
CalendarSyncAdapter.requestCalendarSync(MainActivity.this);
if (filesCount == null) {
showToast(getString(R.string.settings_synchronize_failed));
} else if (filesCount > 0) {
showToast(getString(R.string.settings_synchronize_successful, filesCount));
// Reset internal file caches
WikiEngineApplication app = (WikiEngineApplication) getApplication();
app.resetServices();
} else {
showToast(getString(R.string.settings_synchronize_not_necessary));
}
hideProgressBar();
updateLayoutVisibility();
}
}
private static class ProgressData {
public int step;
public int total;
}
}
@@ -0,0 +1,42 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import org.jetbrains.annotations.Nullable;
/**
* Settings dialog
*/
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_layout);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_layout, new SettingsFragment())
.commit();
}
}
@@ -0,0 +1,203 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.InputType;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import net.moasdawiki.base.Settings;
import net.moasdawiki.service.repository.RepositoryService;
import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "SettingsFragment";
private SynchronizeWikiClient synchronizeWikiClient;
private RepositoryService repositoryService;
private Settings settings;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//noinspection ConstantConditions
WikiEngineApplication app = (WikiEngineApplication) getContext().getApplicationContext();
synchronizeWikiClient = app.getSynchronizeWikiClient();
repositoryService = app.getServiceLocator().getRepositoryService();
settings = app.getServiceLocator().getSettings();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.settings_fragment, rootKey);
EditTextPreference preference = findPreference("sync_server_port");
if (preference != null) {
preference.setOnBindEditTextListener(editText -> {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(5)});
});
}
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
updateSettingsSummaries();
updateStatusText();
}
@Override
public void onPause() {
super.onPause();
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
updateSettingsSummaries();
if ("sync_server_host".equals(key) || "sync_server_port".equals(key)) {
checkServerConnection();
}
}
private void showToast(String message) {
Toast toast = Toast.makeText(getContext(), message, Toast.LENGTH_SHORT);
toast.show();
}
private void updateSettingsSummaries() {
// Host name
SharedPreferences preferences = getPreferenceManager().getSharedPreferences();
String serverHost = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST, null);
EditTextPreference syncServerHost = findPreference("sync_server_host");
if (syncServerHost != null) {
if (serverHost != null && !serverHost.isEmpty()) {
syncServerHost.setSummary(serverHost);
} else {
syncServerHost.setSummary(R.string.settings_host_summary_empty);
}
}
// Port
int serverPort = preferences.getInt(Constants.PREFERENCES_SYNC_SERVER_PORT, 0);
EditTextPreference syncServerPort = findPreference("sync_server_port");
if (syncServerPort != null) {
if (serverPort > 0) {
syncServerPort.setSummary(Integer.toString(serverPort));
} else {
String summary = getString(R.string.settings_port_summary_empty, settings.getServerPort());
syncServerPort.setSummary(summary);
}
}
}
private void updateStatusText() {
SharedPreferences preferences = getPreferenceManager().getSharedPreferences();
String serverSessionId = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID, null);
boolean serverSessionAuthorized = preferences.getBoolean(Constants.PREFERENCES_SYNC_SERVER_SESSION_AUTHORIZED, false);
String serverHost = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST, null);
String serverHostDisplayName = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST_DISPLAYNAME, "");
// Connection status
String connectionStatus;
if (serverSessionId == null) {
connectionStatus = getString(R.string.settings_status_server_not_connected);
} else if (!serverSessionAuthorized) {
connectionStatus = getString(R.string.settings_status_server_authorization_mission);
} else {
connectionStatus = getString(R.string.settings_status_server_connected);
}
// Server Host
String serverHostCombined = serverHostDisplayName;
if (!serverHostCombined.isEmpty() && serverHost != null) {
serverHostCombined += " (" + serverHost + ")";
}
// Last synchronization timestamp
long lastSyncTimeMs = preferences.getLong(Constants.PREFERENCES_SYNC_SERVER_TIME, 0);
String lastSyncStr = "";
if (lastSyncTimeMs > 0) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
lastSyncStr = df.format(new Date(lastSyncTimeMs));
}
// Number of files in repository
int filesCount = repositoryService.getFiles().size();
String filesCountStr = Integer.toString(filesCount);
String htmlText = getString(R.string.settings_status_details, connectionStatus, serverHostCombined, lastSyncStr, filesCountStr);
Preference synchronizationStatus = findPreference("synchronization_status");
if (synchronizationStatus != null) {
synchronizationStatus.setSummary(htmlText);
}
}
private void checkServerConnection() {
new ConnectServerTask().execute();
}
/**
* Checks the connection to the Wiki server.
*/
@SuppressLint("StaticFieldLeak")
@SuppressWarnings("NonStaticInnerClassInSecureContext")
private class ConnectServerTask extends AsyncTask<Void, Void, Boolean> {
@Override
@NotNull
protected Boolean doInBackground(Void... params) {
SharedPreferences preferences = getPreferenceManager().getSharedPreferences();
String host = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST, null);
if (host == null) {
Log.d(TAG, "Cancel ConnectServerTask because server host is not configured");
return false;
}
// Connect with server
return synchronizeWikiClient.createAndCheckSession();
}
@Override
protected void onPostExecute(@NotNull Boolean success) {
updateStatusText();
if (!success) {
showToast(getString(R.string.settings_search_failed));
}
}
}
}
@@ -0,0 +1,466 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Base64;
import android.util.Log;
import androidx.preference.PreferenceManager;
import net.moasdawiki.base.Logger;
import net.moasdawiki.base.ServiceException;
import net.moasdawiki.base.Settings;
import net.moasdawiki.plugin.ServiceLocator;
import net.moasdawiki.plugin.sync.AbstractSyncXml;
import net.moasdawiki.plugin.sync.CheckSessionResponseXml;
import net.moasdawiki.plugin.sync.CheckSessionXml;
import net.moasdawiki.plugin.sync.CreateSessionResponseXml;
import net.moasdawiki.plugin.sync.CreateSessionXml;
import net.moasdawiki.plugin.sync.ErrorResponseXml;
import net.moasdawiki.plugin.sync.ListModifiedFilesResponseXml;
import net.moasdawiki.plugin.sync.ListModifiedFilesXml;
import net.moasdawiki.plugin.sync.ReadFileResponseXml;
import net.moasdawiki.plugin.sync.ReadFileXml;
import net.moasdawiki.plugin.sync.SingleFileXml;
import net.moasdawiki.service.repository.AnyFile;
import net.moasdawiki.service.repository.RepositoryService;
import net.moasdawiki.util.DateUtils;
import net.moasdawiki.util.xml.XmlGenerator;
import net.moasdawiki.util.xml.XmlParser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
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..
*
* @author Herbert Reiter
*/
public class SynchronizeWikiClient {
private static final String TAG = "SynchronizeWikiClient";
private static final String PROTOCOL_VERSION = "2.0";
@NotNull
private final Context mContext;
@NotNull
private final Logger logger;
@NotNull
private final Settings settings;
@NotNull
private final RepositoryService repositoryService;
@NotNull
private final SecureRandom random = new SecureRandom();
public SynchronizeWikiClient(@NotNull Context mContext, @NotNull ServiceLocator serviceLocator) {
this.mContext = mContext;
this.logger = serviceLocator.getLogger();
this.settings = serviceLocator.getSettings();
this.repositoryService = serviceLocator.getRepositoryService();
}
/**
* Verbindet sich mit dem Wikiserver. Ist bereits eine Serversession vorhanden, wird diese
* weiter verwendet.
*/
public boolean createAndCheckSession() {
PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
String host = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST, null);
int port = preferences.getInt(Constants.PREFERENCES_SYNC_SERVER_PORT, settings.getServerPort());
String serverSessionId = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID, null);
// Host konfiguriert?
if (host == null) {
return false;
}
String serverHostPort = host + ':' + port;
try {
// Wenn noch keine Session vorhanden ist, eine erzeugen
boolean createSessionCalled = false;
if (serverSessionId == null) {
createSessionCalled = true;
createSession(serverHostPort);
}
// Ist Session freigeschaltet zur Synchronisierung?
boolean[] checkResult = checkSession(serverHostPort);
if (checkResult[0]) {
return true;
}
// Session ist ungültig --> neue Session holen
if (createSessionCalled) {
// createSession nicht erneut aufrufen
return false;
}
createSession(serverHostPort);
// Session erneut prüfen
boolean[] checkResult2 = checkSession(serverHostPort);
return checkResult2[0];
}
catch (ServiceException e) {
return false;
}
}
private void createSession(@NotNull String serverHostPort) throws ServiceException {
// Anfrage schicken
CreateSessionXml createSessionXml = new CreateSessionXml();
createSessionXml.version = PROTOCOL_VERSION;
createSessionXml.clientSessionId = generateSessionId();
createSessionXml.clientName = "MoasdaWiki-App";
createSessionXml.clientVersion = settings.getVersion();
createSessionXml.clientHost = getDeviceName();
String hostName = getLocalHostname();
if (hostName != null) {
createSessionXml.clientHost += " / " + hostName;
}
String requestXml = generateXml(createSessionXml);
String responseXml = sendXmlRequest(serverHostPort, "/sync/create-session", requestXml);
// Antwort auswerten
CreateSessionResponseXml response = parseXml(responseXml, CreateSessionResponseXml.class);
Log.d(TAG, "Current sync session ID '" + response.serverSessionId + "'");
// Session prüfen
if (response.serverSessionId == null) {
throw new ServiceException("Didn't get a server session ID");
}
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = settings.edit();
editor.putString(Constants.PREFERENCES_SYNC_SERVER_NAME, response.serverName);
editor.putString(Constants.PREFERENCES_SYNC_SERVER_VERSION, response.serverVersion);
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
editor.remove(Constants.PREFERENCES_SYNC_SERVER_TIME);
editor.remove(Constants.PREFERENCES_SYNC_SERVER_SESSION_AUTHORIZED);
editor.apply();
}
private String generateSessionId() {
return new BigInteger(130, random).toString(32);
}
@NotNull
private boolean[] 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
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
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");
SharedPreferences.Editor editor = preferences.edit();
editor.remove(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID);
editor.remove(Constants.PREFERENCES_SYNC_CLIENT_SESSION_ID);
editor.apply();
return new boolean[]{ false, false };
}
if (clientSessionId != null && !clientSessionId.equals(response.clientSessionId)) {
Log.d(TAG, "Sync server authentication failed, client session ID does not match");
SharedPreferences.Editor editor = preferences.edit();
editor.remove(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID);
editor.remove(Constants.PREFERENCES_SYNC_CLIENT_SESSION_ID);
editor.apply();
return new boolean[]{ 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 };
}
/**
* 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.
*/
public int synchronizeRepository(ProgressFeedback feedback) throws ServiceException {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
String host = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST, null);
int port = preferences.getInt(Constants.PREFERENCES_SYNC_SERVER_PORT, settings.getServerPort());
if (host == null) {
throw new ServiceException("No wiki server configured");
}
String serverSessionId = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_SESSION_ID, null);
if (serverSessionId == null) {
throw new ServiceException("Not server session ID found");
}
long lastSyncServerTimeMs = preferences.getLong(Constants.PREFERENCES_SYNC_SERVER_TIME, 0);
Date lastSyncServerTime = null;
if (lastSyncServerTimeMs > 0) {
lastSyncServerTime = new Date(lastSyncServerTimeMs);
}
// Aktuelle Session überprüfen
String serverHostPort = host + ':' + port;
boolean[] checkResult = checkSession(serverHostPort);
if (!checkResult[1]) {
throw new ServiceException("Client not authenticated at wiki server");
}
// Anfrage schicken
ListModifiedFilesXml listModifiedFilesXml = new ListModifiedFilesXml();
listModifiedFilesXml.version = PROTOCOL_VERSION;
listModifiedFilesXml.serverSessionId = serverSessionId;
listModifiedFilesXml.lastSyncServerTime = DateUtils.formatUtcDate(lastSyncServerTime);
String requestXml = generateXml(listModifiedFilesXml);
String responseXml = sendXmlRequest(serverHostPort, "/sync/list-modified-files", requestXml);
// Antwort auswerten
ListModifiedFilesResponseXml response = parseXml(responseXml, ListModifiedFilesResponseXml.class);
int fileCount = response.fileList.size();
Log.d(TAG, "Downloading " + fileCount + " files from server");
if (fileCount == 0) {
// no files to download, cancel process
return 0;
}
for (int i = 0; i < fileCount && !feedback.isCancelled(); i++) {
feedback.progress(i, fileCount);
SingleFileXml serverFile = response.fileList.get(i);
try {
downloadFileFromServer(serverHostPort, serverSessionId, serverFile.filePath);
}
catch (ServiceException e) {
Log.w(TAG, "Error reading file from server, ignoring it", e);
}
}
// Reset internal caches
WikiEngineApplication app = (WikiEngineApplication) mContext.getApplicationContext();
app.resetServices();
// Update last sync time
if (!feedback.isCancelled()) {
Date currentServerDate = DateUtils.parseUtcDate(response.currentServerTime);
if (currentServerDate != null) {
long newLastSyncServerTimeMs = currentServerDate.getTime();
SharedPreferences.Editor editor = preferences.edit();
editor.putLong(Constants.PREFERENCES_SYNC_SERVER_TIME, newLastSyncServerTimeMs);
editor.apply();
}
}
return fileCount;
}
private void downloadFileFromServer(@NotNull String serverHostPort, @NotNull String serverSessionId, @NotNull String filePath) throws ServiceException {
// Anfrage schicken
ReadFileXml readFileXml = new ReadFileXml();
readFileXml.version = PROTOCOL_VERSION;
readFileXml.serverSessionId = serverSessionId;
readFileXml.filePath = filePath;
String requestXml = generateXml(readFileXml);
String responseXml = sendXmlRequest(serverHostPort, "/sync/read-file", requestXml);
// Datei aus Antwort speichern
ReadFileResponseXml response = parseXml(responseXml, ReadFileResponseXml.class);
byte[] fileContent = Base64.decode(response.content, Base64.NO_WRAP);
Date fileTimestamp = DateUtils.parseUtcDate(response.timestamp);
AnyFile anyFile = new AnyFile(filePath);
repositoryService.writeBinaryFile(anyFile, fileContent, fileTimestamp);
Log.d(TAG, "File '" + filePath + "' replaced by newer content from server");
}
/**
* Versucht den Wifi-Hostnamen zu ermitteln und gibt ihn zurück.
*/
@Nullable
private String getLocalHostname() {
try {
String wlanHostname = null;
String ethHostname = null;
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface ni = networkInterfaces.nextElement();
Enumeration<InetAddress> inetAddresses = ni.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress ia = inetAddresses.nextElement();
// Indikator: Hostname und Adresse sind unterschiedlich
// http://stackoverflow.com/questions/21898456/get-android-wifi-net-hostname-from-code
if (!ia.getCanonicalHostName().equals(ia.getHostAddress())) {
if ("wlan0".equals(ni.getDisplayName())) {
wlanHostname = ia.getCanonicalHostName();
} else if ("eth0".equals(ia.getHostAddress())) {
ethHostname = ia.getCanonicalHostName();
}
}
}
}
if (wlanHostname != null) {
Log.d(TAG, "Hostname (wlan0): " + wlanHostname);
return wlanHostname;
}
if (ethHostname != null) {
Log.d(TAG, "Hostname (eth0): " + ethHostname);
return ethHostname;
}
Log.d(TAG, "No hostname found");
} catch (SocketException e) {
Log.e(TAG, "Error determining hostname", e);
}
return null;
}
@NotNull
private String getDeviceName() {
String manufacturer = Build.MANUFACTURER;
String model = Build.MODEL;
if (model.startsWith(manufacturer)) {
return capitalize(model);
} else {
return capitalize(manufacturer) + " " + model;
}
}
private String capitalize(String s) {
if (s == null || s.isEmpty()) {
return "";
}
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
/**
* 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.
*/
@NotNull
private String sendXmlRequest(@NotNull String serverHostPort, @NotNull String urlPath, @NotNull String requestXml) throws ServiceException {
try {
String url = "http://" + serverHostPort + urlPath;
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();
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);
}
byte[] responseBytes = byteStream.toByteArray();
String responseXml = new String(responseBytes, StandardCharsets.UTF_8);
Log.d(TAG, "Response: " + truncateLogText(responseXml, 400));
return responseXml;
} catch (Exception e) {
Log.e(TAG, "Error sending XML request", e);
throw new ServiceException("Error sending XML request", e);
}
}
private String truncateLogText(String logText, int maxLength) {
if (logText.length() <= maxLength) {
return logText;
}
return logText.substring(0, maxLength) + '…';
}
/**
* Wandelt eine JAXB-Bean in einen XML-Strom um.
*/
@NotNull
private String generateXml(@NotNull AbstractSyncXml xmlBean) throws ServiceException {
XmlGenerator xmlGenerator = new XmlGenerator(logger);
return xmlGenerator.generate(xmlBean);
}
/**
* Wandelt einen XML-Strom in eine JAXB-Bean um.
*/
@NotNull
private <T extends AbstractSyncXml> T parseXml(@NotNull String xml, @NotNull Class<T> xmlBeanType) throws ServiceException {
try {
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);
// Versuche eine Fehlerantwort zu parsen
XmlParser xmlParser = new XmlParser(logger);
ErrorResponseXml errorResponseXml = xmlParser.parse(xml, ErrorResponseXml.class);
throw new ServiceException(errorResponseXml.message);
}
}
/**
* Interface, um den Fortschritt während der Synchronisierung mit dem Server
* zurückzuübermitteln.
*/
public interface ProgressFeedback {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isCancelled();
void progress(int step, int total);
}
}
@@ -0,0 +1,93 @@
/*
* MoasdaWiki App
* Copyright (C) 2008 - 2020 Herbert Reiter (herbert@moasdawiki.net)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.moasdawiki.app;
import android.app.Application;
import net.moasdawiki.base.Logger;
import net.moasdawiki.base.Messages;
import net.moasdawiki.base.Settings;
import net.moasdawiki.plugin.PluginService;
import net.moasdawiki.plugin.ServiceLocator;
import net.moasdawiki.service.render.HtmlService;
import net.moasdawiki.service.repository.FilesystemRepositoryService;
import net.moasdawiki.service.repository.RepositoryService;
import net.moasdawiki.service.search.SearchService;
import net.moasdawiki.service.wiki.WikiService;
import net.moasdawiki.service.wiki.WikiServiceImpl;
import java.io.File;
/**
* Verwaltet den Lebenszyklus der Wiki Engine. Muss außerhalb der Activities erfolgen, weil diese
* z.B. beim Drehen des Bildschirm neu erzeugt werden.
*
* @author Herbert Reiter
*/
public class WikiEngineApplication extends Application {
private Logger logger;
private ServiceLocator serviceLocator;
private SynchronizeWikiClient synchronizeWikiClient;
@Override
public void onCreate() {
super.onCreate();
logger = new Logger();
logger.write("MoasdaWiki app starting");
File internalStorageRepositoryRoot = new File(getFilesDir(), "repository");
RepositoryService repositoryService = new FilesystemRepositoryService(logger, internalStorageRepositoryRoot);
repositoryService.init();
WikiService wikiService = new WikiServiceImpl(logger, repositoryService);
SearchService searchService = new SearchService(logger, wikiService);
Settings settings = new AndroidSettings(logger, repositoryService, AndroidSettings.getConfigFileApp());
Messages messages = new Messages(logger, settings, repositoryService);
PluginService pluginService = new PluginService(logger, settings);
HtmlService htmlService = new HtmlService(logger, settings, messages, wikiService, pluginService);
serviceLocator = new ServiceLocator(logger, settings, messages, repositoryService, wikiService, htmlService, searchService, pluginService);
pluginService.loadPlugins(serviceLocator);
synchronizeWikiClient = new SynchronizeWikiClient(this, serviceLocator);
}
public void resetServices() {
serviceLocator.getRepositoryService().rebuildCache();
serviceLocator.getWikiService().reset();
serviceLocator.getSettings().reset();
serviceLocator.getMessages().reset();
serviceLocator.getPluginService().loadPlugins(serviceLocator);
}
@Override
public void onTerminate() {
logger.write("MoasdaWiki app stopping");
super.onTerminate();
}
public ServiceLocator getServiceLocator() {
return serviceLocator;
}
public SynchronizeWikiClient getSynchronizeWikiClient() {
return synchronizeWikiClient;
}
}
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="#FF000000"
android:pathData="M17,12h-5v5h5v-5zM16,1v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2h-1L18,1h-2zM19,19L5,19L5,8h14v11z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="#FF000000"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<path
android:fillColor="#FF000000"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
</vector>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_version"
android:id="@+id/label_version" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/about_copyright"
android:id="@+id/label_copyright" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/about_homepage_url"
android:autoLink="web"
android:linksClickable="true" />
</LinearLayout>
+130
View File
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/hint_host_unconfigured"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:onClick="onConfigurationHintClicked">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<View
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_margin="20dp"
android:background="@drawable/ic_info_outline_black" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/main_hint_settings_missing" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="?android:attr/listDivider" />
</LinearLayout>
<LinearLayout
android:id="@+id/hint_repository_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:onClick="onSynchronizeHintClicked">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<View
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_margin="20dp"
android:background="@drawable/ic_info_outline_black" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/main_hint_synchronize" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="?android:attr/listDivider" />
</LinearLayout>
<LinearLayout
android:id="@+id/search_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText android:id="@+id/search_input"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/main_search_input_hint"
android:inputType="text"
android:importantForAutofill="no"
android:imeOptions="actionSearch" />
<ImageButton android:id="@+id/search_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_gravity="center_vertical"
android:background="@drawable/ic_menu_search"
android:onClick="onSearch"
android:contentDescription="@string/main_search_button_description" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="?android:attr/listDivider" />
</LinearLayout>
<ProgressBar
android:id="@+id/search_progressbar"
style="@android:style/Widget.Holo.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateTint="#2324aa"
android:visibility="gone" />
<WebView
android:id="@+id/web_browser"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/settings_layout">
</FrameLayout>
+40
View File
@@ -0,0 +1,40 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_startpage"
android:title="@string/action_startpage"
android:icon="@drawable/ic_menu_home"
android:orderInCategory="100"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_synchronize"
android:title="@string/action_synchronize"
android:icon="@drawable/ic_action_sync_white"
android:orderInCategory="200"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_settings"
android:title="@string/action_settings"
android:icon="@drawable/ic_menu_preferences"
android:orderInCategory="300"
app:showAsAction="never" />
<item
android:id="@+id/action_help"
android:title="@string/action_help"
android:icon="@drawable/ic_menu_help"
android:orderInCategory="400"
app:showAsAction="never" />
<item
android:id="@+id/action_about"
android:title="@string/action_about"
android:icon="@drawable/ic_menu_info_details"
android:orderInCategory="500"
app:showAsAction="never" />
</menu>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_copyright">Copyright %1$d © Herbert Reiter</string>
<string name="about_version">Version %1$s</string>
<string name="action_help">Hilfe</string>
<string name="action_about">Über</string>
<string name="action_back_close_hint">Erneut drücken um die App zu beenden.</string>
<string name="action_settings">Einstellungen</string>
<string name="action_startpage">Startseite</string>
<string name="action_synchronize">Synchronisieren</string>
<string name="button_close">Schließen</string>
<string name="calendar_display_name">MoasdaWiki Termine</string>
<string name="calendar_date">Datum: </string>
<string name="calendar_permission_request">Bitte erteile die Berechtigung %1$s damit es funktioniert</string>
<string name="calendar_sync_finished">Kalender fertig aktualisiert</string>
<string name="main_hint_settings_missing">Vielen Dank für die Installation der MoasdaWiki App. Bitte konfiguriere nun die Serververbindung, um die Wikiseiten synchronisieren zu können. Drücke hier um die Einstellungen zu öffnen.</string>
<string name="main_hint_synchronize">Die App hat noch keine Wikiseiten. Drücke hier um jetzt mit dem MoasdaWiki-Server zu synchronisieren.</string>
<string name="main_search_input_hint">Durchsuche Wiki</string>
<string name="main_search_button_description">Suchen</string>
<string name="settings_title">Einstellungen</string>
<string name="settings_synchronize">Mit Wikiserver synchronisieren</string>
<string name="settings_host_title">Hostname</string>
<string name="settings_host_summary_empty">Gib den Hostnamen des Rechners auf dem der MoasdaWiki server läuft an.</string>
<string name="settings_port">Host-Port</string>
<string name="settings_port_summary_empty">%1$d (Standardwert)</string>
<string name="settings_status">Status der Synchronisierung</string>
<string name="settings_status_details"><![CDATA[Verbindung: %1$s\nServer-Host: %2$s\nLetzte Synchronisierung: %3$s\nDateien im lokalen Repository: %4$s]]></string>
<string name="settings_status_server_not_connected">Nicht verbunden</string>
<string name="settings_status_server_connected">Am Server angemeldet</string>
<string name="settings_status_server_authorization_mission">Erfordert Berechtigung am Server</string>
<string name="settings_search_failed">Keinen Wikiserver gefunden!</string>
<string name="settings_synchronize_successful">%1$d Dateien erfolgreich synchronisiert</string>
<string name="settings_synchronize_not_necessary">Dateien sind bereits aktuell</string>
<string name="settings_synchronize_failed">Synchronisierung nicht möglich, bitte überprüfen Sie die Einstellungen und den Status!</string>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
</resources>
+36
View File
@@ -0,0 +1,36 @@
<resources>
<string name="app_name" translatable="false">MoasdaWiki App</string>
<string name="about_copyright">Copyright %1$d © Herbert Reiter</string>
<string name="about_homepage_url" translatable="false">https://www.moasdawiki.net/</string>
<string name="about_version">Version %1$s</string>
<string name="action_about">About</string>
<string name="action_back_close_hint">Press again to exit the app.</string>
<string name="action_help">Help</string>
<string name="action_settings">Settings</string>
<string name="action_startpage">Start page</string>
<string name="action_synchronize">Synchronize</string>
<string name="button_close">Close</string>
<string name="calendar_display_name">MoasdaWiki Events</string>
<string name="calendar_date">Date</string>
<string name="calendar_permission_request">Please grant permission %1$s to make it work</string>
<string name="calendar_sync_finished">Calendar update finished</string>
<string name="main_hint_settings_missing">Thank you for installing the MoasdaWiki App. Please configure the server connection to synchronize the Wiki pages. Tap here to go to settings.</string>
<string name="main_hint_synchronize">The App has still no Wiki content. Tap here to synchronize with the MoasdaWiki server now.</string>
<string name="main_search_input_hint">Search in Wiki pages</string>
<string name="main_search_button_description">Search</string>
<string name="settings_title">Settings</string>
<string name="settings_synchronize">Synchronize with Wiki server</string>
<string name="settings_host_title">Host name</string>
<string name="settings_host_summary_empty">Enter the host name of a MoasdaWiki server instance.</string>
<string name="settings_port">Host port</string>
<string name="settings_port_summary_empty">%1$d (default value)</string>
<string name="settings_status">Synchronization status</string>
<string name="settings_status_details"><![CDATA[Connection: %1$s\nServer host: %2$s\nLast synchronization: %3$s\nFiles in local repository: %4$s]]></string>
<string name="settings_status_server_not_connected">Not connected</string>
<string name="settings_status_server_connected">Connected to server</string>
<string name="settings_status_server_authorization_mission">Needs authorization at server</string>
<string name="settings_search_failed">No Wiki server found!</string>
<string name="settings_synchronize_successful">Successfully synchronized %1$d files</string>
<string name="settings_synchronize_not_necessary">All files are up-to-date</string>
<string name="settings_synchronize_failed">Synchronization failed, please check settings and status!</string>
</resources>
+12
View File
@@ -0,0 +1,12 @@
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="actionBarStyle">@style/MyActionBarLogo</item>
</style>
<style name="MyActionBarLogo" parent="@style/Widget.AppCompat.Light.ActionBar.Solid.Inverse">
<item name="logo">@mipmap/ic_cow</item>
<item name="displayOptions">useLogo|showTitle|showHome</item>
</style>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="net.moasdawiki"
android:icon="@mipmap/ic_cow"
android:smallIcon="@mipmap/ic_cow"
android:label="@string/app_name" />
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent">
<PreferenceCategory
android:title="@string/settings_synchronize"
android:icon="@drawable/ic_action_sync">
<EditTextPreference
android:key="sync_server_host"
android:persistent="true"
android:title="@string/settings_host_title"
android:summary="@string/settings_host_summary_empty" />
<EditTextPreference
android:key="sync_server_port"
android:persistent="true"
android:title="@string/settings_port"
android:inputType="number" />
<Preference
android:key="synchronization_status"
android:persistent="false"
android:selectable="false"
android:title="@string/settings_status" />
</PreferenceCategory>
<PreferenceCategory
android:title="Calendar integration"
android:icon="@drawable/ic_action_event">
<SwitchPreferenceCompat
android:key="calendar_enabled"
android:persistent="true"
android:title="Event calendar"
android:summary="Provide all Wiki contact birthdays and tasks with deadline as calendar events." />
</PreferenceCategory>
</PreferenceScreen>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="net.moasdawiki.app.provider"
android:accountType="net.moasdawiki"
android:userVisible="false"
android:supportsUploading="false"
android:allowParallelSyncs="false"
android:isAlwaysSyncable="true" />