Initial check-in
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user