You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java

202 lines
7.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright (c) 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.http.HttpStatus;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.Closeable;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.net.ssl.SSLException;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalStorageException;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.webdav.DavException;
import at.bitfire.davdroid.webdav.DavHttpClient;
import at.bitfire.davdroid.webdav.HttpException;
import lombok.Getter;
public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter implements Closeable {
private final static String TAG = "davdroid.DavSyncAdapter";
@Getter private static String androidID;
protected Context context;
/* We use one static httpClient for
* - all sync adapters (CalendarsSyncAdapter, ContactsSyncAdapter)
* - and all threads (= accounts) of each sync adapter
* so that HttpClient's threaded pool management can do its best.
*/
protected static CloseableHttpClient httpClient;
/* One static read/write lock pair for the static httpClient:
* Use the READ lock when httpClient will only be called (to prevent it from being unset while being used).
* Use the WRITE lock when httpClient will be modified (set/unset). */
private final static ReentrantReadWriteLock httpClientLock = new ReentrantReadWriteLock();
public DavSyncAdapter(Context context) {
super(context, true);
synchronized(this) {
if (androidID == null)
androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
}
this.context = context;
}
@Override
public void close() {
Log.d(TAG, "Closing httpClient");
// may be called from a GUI thread, so we need an AsyncTask
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try {
httpClientLock.writeLock().lock();
if (httpClient != null) {
httpClient.close();
httpClient = null;
}
httpClientLock.writeLock().unlock();
} catch (IOException e) {
Log.w(TAG, "Couldn't close HTTP client", e);
}
return null;
}
}.execute();
}
protected abstract Map<LocalCollection<?>, RemoteCollection<?>> getSyncPairs(Account account, ContentProviderClient provider);
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.i(TAG, "Performing sync for authority " + authority);
// set class loader for iCal4j ResourceLoader
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
// create httpClient, if necessary
httpClientLock.writeLock().lock();
if (httpClient == null) {
Log.d(TAG, "Creating new DavHttpClient");
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getContext());
httpClient = DavHttpClient.create();
}
// prevent httpClient shutdown until we're ready by holding a read lock
// acquiring read lock before releasing write lock will downgrade the write lock to a read lock
httpClientLock.readLock().lock();
httpClientLock.writeLock().unlock();
// TODO use VCard 4.0 if possible
AccountSettings accountSettings = new AccountSettings(getContext(), account);
Log.d(TAG, "Server supports VCard version " + accountSettings.getAddressBookVCardVersion());
Exception exceptionToShow = null; // exception to show notification for
try {
// get local <-> remote collection pairs
Map<LocalCollection<?>, RemoteCollection<?>> syncCollections = getSyncPairs(account, provider);
if (syncCollections == null)
Log.i(TAG, "Nothing to synchronize");
else
try {
for (Map.Entry<LocalCollection<?>, RemoteCollection<?>> entry : syncCollections.entrySet())
new SyncManager(entry.getKey(), entry.getValue()).synchronize(extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);
} catch (DavException ex) {
exceptionToShow = ex;
syncResult.stats.numParseExceptions++;
Log.e(TAG, "Invalid DAV response", ex);
} catch (HttpException ex) {
if (ex.getCode() == HttpStatus.SC_UNAUTHORIZED) {
exceptionToShow = ex;
Log.e(TAG, "HTTP Unauthorized " + ex.getCode(), ex);
syncResult.stats.numAuthExceptions++; // hard error
} else if (ex.isClientError()) {
exceptionToShow = ex;
Log.e(TAG, "Hard HTTP error " + ex.getCode(), ex);
syncResult.stats.numParseExceptions++; // hard error
} else {
Log.w(TAG, "Soft HTTP error " + ex.getCode() + " (Android will try again later)", ex);
syncResult.stats.numIoExceptions++; // soft error
}
} catch (LocalStorageException ex) {
exceptionToShow = ex;
syncResult.databaseError = true; // hard error
Log.e(TAG, "Local storage (content provider) exception", ex);
} catch (IOException ex) {
syncResult.stats.numIoExceptions++; // soft error
Log.e(TAG, "I/O error (Android will try again later)", ex);
} catch (URISyntaxException ex) {
exceptionToShow = ex;
syncResult.stats.numParseExceptions++; // hard error
Log.e(TAG, "Invalid URI (file name) syntax", ex);
}
} finally {
// allow httpClient shutdown
httpClientLock.readLock().unlock();
}
// show sync errors as notification
if (exceptionToShow != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Intent intentHelp = new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.WEB_URL_VIEW_LOGS));
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, intentHelp, 0);
Notification.Builder builder = new Notification.Builder(context)
.setSmallIcon(R.drawable.ic_launcher)
.setPriority(Notification.PRIORITY_LOW)
.setOnlyAlertOnce(true)
.setWhen(System.currentTimeMillis())
.setContentTitle(context.getString(R.string.sync_error_title))
.setContentText(exceptionToShow.getLocalizedMessage())
.setContentInfo(account.name)
.setStyle(new Notification.BigTextStyle().bigText(account.name + ":\n" + ExceptionUtils.getFullStackTrace(exceptionToShow)))
.setContentIntent(contentIntent);
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(account.name.hashCode(), builder.build());
}
Log.i(TAG, "Sync complete for " + authority);
}
}