com.jefftharris.passwdsafe.sync.gdrive.GDriveProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.jefftharris.passwdsafe.sync.gdrive.GDriveProvider.java

Source

/*
 * Copyright () 2013-2014 Jeff Harris <jefftharris@gmail.com> All rights reserved.
 * Use of the code is allowed under the Artistic License 2.0 terms, as specified
 * in the LICENSE file distributed with this code, or available from
 * http://www.opensource.org/licenses/artistic-license-2.0.php
 */
package com.jefftharris.passwdsafe.sync.gdrive;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.UserRecoverableNotifiedException;
import com.google.android.gms.common.AccountPicker;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.extensions.android.accounts.GoogleAccountManager;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAuthIOException;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.ExponentialBackOff;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.DriveScopes;
import com.jefftharris.passwdsafe.lib.ApiCompat;
import com.jefftharris.passwdsafe.lib.PasswdSafeContract;
import com.jefftharris.passwdsafe.lib.PasswdSafeUtil;
import com.jefftharris.passwdsafe.lib.ProviderType;
import com.jefftharris.passwdsafe.sync.R;
import com.jefftharris.passwdsafe.sync.SyncApp;
import com.jefftharris.passwdsafe.sync.SyncUpdateHandler;
import com.jefftharris.passwdsafe.sync.lib.AbstractProvider;
import com.jefftharris.passwdsafe.sync.lib.DbProvider;
import com.jefftharris.passwdsafe.sync.lib.NewAccountTask;
import com.jefftharris.passwdsafe.sync.lib.SyncDb;
import com.jefftharris.passwdsafe.sync.lib.SyncLogRecord;

import java.io.IOException;
import java.util.Collections;

/**
 * The GDriveProvider class encapsulates Google Drive
 */
public class GDriveProvider extends AbstractProvider {
    public static final String ABOUT_FIELDS = "user";
    public static final String FILE_FIELDS = "id,name,mimeType,trashed,fileExtension,modifiedTime,"
            + "md5Checksum,parents";
    public static final String FOLDER_MIME = "application/vnd.google-apps.folder";

    private static final String PREF_ACCOUNT_NAME = "gdriveAccountName";
    private static final String PREF_MIGRATION = "gdriveMigration";

    private static final int MIGRATION_V3API = 1;

    private static final String TAG = "GDriveProvider";

    private final Context itsContext;
    private String itsAccountName;

    /** Constructor */
    public GDriveProvider(Context ctx) {
        itsContext = ctx;
    }

    @Override
    public void init() {
        checkMigration();
        updateAcct();
        requestSync(false);
    }

    @Override
    public void fini() {
    }

    @Override
    public void startAccountLink(FragmentActivity activity, int requestCode) {
        Intent intent = AccountPicker.newChooseAccountIntent(null, null,
                new String[] { GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE }, true, null, null, null, null);
        try {
            activity.startActivityForResult(intent, requestCode);
        } catch (ActivityNotFoundException e) {
            String msg = itsContext.getString(R.string.google_acct_not_available);
            Log.e(TAG, msg, e);
            PasswdSafeUtil.showErrorMsg(msg, activity);
        }
    }

    @Override
    public NewAccountTask finishAccountLink(int activityResult, Intent activityData, Uri acctProviderUri) {
        if (activityData == null) {
            return null;
        }

        Bundle b = activityData.getExtras();
        String accountName = b.getString(AccountManager.KEY_ACCOUNT_NAME);
        Log.i(TAG, "Selected account: " + accountName);
        if (TextUtils.isEmpty(accountName)) {
            return null;
        }

        setAcctName(accountName);
        updateAcct();
        return new NewAccountTask(acctProviderUri, accountName, ProviderType.GDRIVE, false, itsContext);
    }

    @Override
    public void unlinkAccount() {
    }

    @Override
    public synchronized boolean isAccountAuthorized() {
        return !TextUtils.isEmpty(itsAccountName);
    }

    @Override
    public Account getAccount(String acctName) {
        GoogleAccountManager acctMgr = new GoogleAccountManager(itsContext);
        return acctMgr.getAccountByName(acctName);
    }

    @Override
    public void checkProviderAdd(SQLiteDatabase db) throws Exception {
    }

    @Override
    public void cleanupOnDelete(String acctName) {
        Account acct = getAccount(acctName);
        setAcctName(null);
        updateAcct();
        try {
            GoogleAccountCredential credential = getAcctCredential(itsContext);
            String token = GoogleAuthUtil.getTokenWithNotification(itsContext, acct, credential.getScope(), null);
            PasswdSafeUtil.dbginfo(TAG, "Remove token for %s", acctName);
            if (token != null) {
                GoogleAuthUtil.clearToken(itsContext, token);
            }
        } catch (Exception e) {
            PasswdSafeUtil.dbginfo(TAG, e, "No auth token for %s", acctName);
        }
    }

    @Override
    public void updateSyncFreq(Account acct, int freq) {
        if (acct != null) {
            ContentResolver.removePeriodicSync(acct, PasswdSafeContract.AUTHORITY, new Bundle());
            ContentResolver.setSyncAutomatically(acct, PasswdSafeContract.AUTHORITY, false);
            if (freq > 0) {
                ContentResolver.setSyncAutomatically(acct, PasswdSafeContract.AUTHORITY, true);
                ContentResolver.addPeriodicSync(acct, PasswdSafeContract.AUTHORITY, new Bundle(), freq);
            }
        }
    }

    @Override
    public synchronized void requestSync(boolean manual) {
        PasswdSafeUtil.dbginfo(TAG, "requestSync manual %b", manual);
        if (isAccountAuthorized()) {
            Account acct = getAccount(itsAccountName);
            Bundle extras = new Bundle();
            ApiCompat.requestManualSync(acct, PasswdSafeContract.CONTENT_URI, extras);
        }
    }

    @Override
    public void sync(Account acct, DbProvider provider, SQLiteDatabase db, SyncLogRecord logrec) throws Exception {
        Pair<Drive, String> driveService = getDriveService(acct, itsContext);

        GDriveSyncer sync = new GDriveSyncer(driveService.first, provider, db, logrec, itsContext);
        SyncUpdateHandler.GDriveState syncState = SyncUpdateHandler.GDriveState.OK;
        try {
            sync.sync();
            syncState = sync.getSyncState();
        } catch (UserRecoverableAuthIOException e) {
            PasswdSafeUtil.dbginfo(TAG, e, "Recoverable google auth error");
            GoogleAuthUtil.clearToken(itsContext, driveService.second);
            syncState = SyncUpdateHandler.GDriveState.AUTH_REQUIRED;
        } catch (GoogleAuthIOException e) {
            Log.e(TAG, "Google auth error", e);
            GoogleAuthUtil.clearToken(itsContext, driveService.second);
            throw e;
        } finally {
            SyncApp.get(itsContext).updateGDriveSyncState(syncState);
        }
    }

    /**
     * Set the account name
     */
    private synchronized void setAcctName(String acctName) {
        PasswdSafeUtil.dbginfo(TAG, "setAcctName %s", acctName);
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(itsContext);
        prefs.edit().putString(PREF_ACCOUNT_NAME, acctName).apply();
    }

    /**
     * Update the account from saved authentication information
     */
    private synchronized void updateAcct() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(itsContext);
        itsAccountName = prefs.getString(PREF_ACCOUNT_NAME, null);
    }

    /**
     * Check whether any migrations are needed
     */
    private void checkMigration() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(itsContext);
        int migration = prefs.getInt(PREF_MIGRATION, 0);

        if (migration < MIGRATION_V3API) {
            // Set the account name from the db provider
            SyncDb syncDb = SyncDb.acquire();
            try {
                SQLiteDatabase db = syncDb.beginTransaction();
                for (DbProvider provider : SyncDb.getProviders(db)) {
                    if (provider.itsType != ProviderType.GDRIVE) {
                        continue;
                    }

                    setAcctName(provider.itsAcct);
                }
                db.setTransactionSuccessful();
            } catch (SQLException e) {
                Log.e(TAG, "Error migrating account", e);
            } finally {
                syncDb.endTransactionAndRelease();
            }

            prefs.edit().putInt(PREF_MIGRATION, MIGRATION_V3API).apply();
        }
    }

    /** Get the Google account credential */
    private static GoogleAccountCredential getAcctCredential(Context ctx) {
        return GoogleAccountCredential.usingOAuth2(ctx, Collections.singletonList(DriveScopes.DRIVE));
    }

    /**
     * Retrieve a authorized service object to send requests to the Google
     * Drive API. On failure to retrieve an access token, a notification is
     * sent to the user requesting that authorization be granted for the
     * {@code https://www.googleapis.com/auth/drive} scope.
     *
     * @return An authorized service object and its auth token.
     */
    private static Pair<Drive, String> getDriveService(Account acct, Context ctx) {
        Drive drive = null;
        String token = null;
        try {
            GoogleAccountCredential credential = getAcctCredential(ctx);
            credential.setBackOff(new ExponentialBackOff());
            credential.setSelectedAccountName(acct.name);

            token = GoogleAuthUtil.getTokenWithNotification(ctx, acct, credential.getScope(), null,
                    PasswdSafeContract.AUTHORITY, null);

            Drive.Builder builder = new Drive.Builder(AndroidHttp.newCompatibleTransport(),
                    JacksonFactory.getDefaultInstance(), credential);
            builder.setApplicationName(ctx.getString(R.string.app_name));
            drive = builder.build();
        } catch (UserRecoverableNotifiedException e) {
            // User notified
            PasswdSafeUtil.dbginfo(TAG, e, "User notified auth exception");
            try {
                GoogleAuthUtil.clearToken(ctx, null);
            } catch (Exception ioe) {
                Log.e(TAG, "getDriveService clear failure", e);
            }
        } catch (GoogleAuthException e) {
            // Unrecoverable
            Log.e(TAG, "Unrecoverable auth exception", e);
        } catch (IOException e) {
            // Transient
            PasswdSafeUtil.dbginfo(TAG, e, "Transient error");
        } catch (Exception e) {
            Log.e(TAG, "Token exception", e);
        }
        return new Pair<>(drive, token);
    }
}