at.bitfire.davdroid.syncadapter.DavSyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for at.bitfire.davdroid.syncadapter.DavSyncAdapter.java

Source

/*
 * Copyright  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.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.SyncResult;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;

import org.apache.commons.lang3.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.WebDavCollection;
import at.bitfire.davdroid.ui.settings.AccountActivity;
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;

    final 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<?>, WebDavCollection<?>> 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  this is required because the various
         * sync adapters (contacts, events, tasks) share the same :sync process (see AndroidManifest */
        Thread.currentThread().setContextClassLoader(getContext().getClassLoader());

        // create httpClient, if necessary
        httpClientLock.writeLock().lock();
        if (httpClient == null) {
            Log.d(TAG, "Creating new DavHttpClient");
            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();

        Exception exceptionToShow = null; // exception to show notification for
        Intent exceptionIntent = null; // what shall happen when clicking on the exception notification
        try {
            // get local <-> remote collection pairs
            Map<LocalCollection<?>, WebDavCollection<?>> syncCollections = getSyncPairs(account, provider);
            if (syncCollections == null)
                Log.i(TAG, "Nothing to synchronize");
            else
                try {
                    for (Map.Entry<LocalCollection<?>, WebDavCollection<?>> entry : syncCollections.entrySet())
                        new SyncManager(entry.getKey(), entry.getValue())
                                .synchronize(extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);

                } catch (DavException ex) {
                    syncResult.stats.numParseExceptions++;
                    Log.e(TAG, "Invalid DAV response", ex);
                    exceptionToShow = ex;

                } catch (HttpException ex) {
                    if (ex.getCode() == HttpStatus.SC_UNAUTHORIZED) {
                        Log.e(TAG, "HTTP Unauthorized " + ex.getCode(), ex);
                        syncResult.stats.numAuthExceptions++; // hard error

                        exceptionToShow = ex;
                        exceptionIntent = new Intent(context, AccountActivity.class);
                        exceptionIntent.putExtra(AccountActivity.EXTRA_ACCOUNT, account);
                    } else if (ex.isClientError()) {
                        Log.e(TAG, "Hard HTTP error " + ex.getCode(), ex);
                        syncResult.stats.numParseExceptions++; // hard error
                        exceptionToShow = ex;
                    } else {
                        Log.w(TAG, "Soft HTTP error " + ex.getCode() + " (Android will try again later)", ex);
                        syncResult.stats.numIoExceptions++; // soft error
                    }
                } catch (LocalStorageException ex) {
                    syncResult.databaseError = true; // hard error
                    Log.e(TAG, "Local storage (content provider) exception", ex);
                    exceptionToShow = ex;
                } catch (IOException ex) {
                    syncResult.stats.numIoExceptions++; // soft error
                    Log.e(TAG, "I/O error (Android will try again later)", ex);
                    if (ex instanceof SSLException) // always notify on SSL/TLS errors
                        exceptionToShow = ex;
                } catch (URISyntaxException ex) {
                    syncResult.stats.numParseExceptions++; // hard error
                    Log.e(TAG, "Invalid URI (file name) syntax", ex);
                    exceptionToShow = 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) {
            if (exceptionIntent == null)
                exceptionIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.WEB_URL_VIEW_LOGS));

            PendingIntent contentIntent = PendingIntent.getActivity(context, 0, exceptionIntent, 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.getStackTrace(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);
    }

}