Initial check-in
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 255 B |
|
After Width: | Height: | Size: 503 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 159 B |
|
After Width: | Height: | Size: 334 B |
|
After Width: | Height: | Size: 289 B |
|
After Width: | Height: | Size: 951 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 225 B |
|
After Width: | Height: | Size: 623 B |
|
After Width: | Height: | Size: 543 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 918 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
</resources>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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" />
|
||||||