com.weebly.opus1269.clipman.backup.DriveServiceHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.weebly.opus1269.clipman.backup.DriveServiceHelper.java

Source

/*
 * Copyright (c) 2016-2017, Michael A. Updike All rights reserved.
 * Licensed under Apache 2.0
 * https://opensource.org/licenses/Apache-2.0
 * https://github.com/Pushy-Clipboard/pushy-android/blob/master/LICENSE.md
 */

package com.weebly.opus1269.clipman.backup;

import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;

import com.google.android.gms.common.api.Scope;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.File;

import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.api.services.drive.model.FileList;
import com.weebly.opus1269.clipman.R;
import com.weebly.opus1269.clipman.app.App;
import com.weebly.opus1269.clipman.app.Log;
import com.weebly.opus1269.clipman.db.entity.Backup;
import com.weebly.opus1269.clipman.model.BackupContents;
import com.weebly.opus1269.clipman.model.User;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import androidx.annotation.NonNull;

/** Singleton to manage interactions with Google Drive */
public class DriveServiceHelper {

    // OK, because mAppCtxt is the global Application context
    /** Static instance */
    @SuppressLint("StaticFieldLeak")
    private static DriveServiceHelper sInstance;

    /** Global Application Context */
    private final Context mAppCtxt;

    /** Timeout for task completion */
    private final int WAIT_TIME_SECS = 20;

    /** Mime type of backups */
    private final String MIME_TYPE = "application/zip";

    /** No permissions for Drive */
    private final String ERR_PERMISSION;

    /** Error accessing Drive API */
    private final String ERR_INTERNAL;

    /** Class Indentifier */
    private final String TAG = this.getClass().getSimpleName();

    /** App folder on Drive */
    private final String APP_FOLDER = "appDataFolder";

    private DriveServiceHelper(@NonNull Context context) {
        mAppCtxt = context.getApplicationContext();
        ERR_INTERNAL = mAppCtxt.getString(R.string.err_internal_drive);
        ERR_PERMISSION = mAppCtxt.getString(R.string.err_drive_scope_denied);
    }

    /**
     * Lazily create our instance
     * @param context any old context
     */
    public static DriveServiceHelper INST(@NonNull Context context) {
        synchronized (DriveServiceHelper.class) {
            if (sInstance == null) {
                sInstance = new DriveServiceHelper(context);
            }
            return sInstance;
        }
    }

    /** No permission for our appFolder */
    public boolean noAppFolderPermission() {
        final GoogleSignInAccount account = User.INST(mAppCtxt).getGoogleAccount();
        return ((account == null) || !GoogleSignIn.hasPermissions(account, new Scope(DriveScopes.DRIVE_APPDATA)));
    }

    /**
     * Retrieve all the backups in our appFolder
     * @return list of backups
     */
    @NonNull
    public List<Backup> getBackups()
            throws ExecutionException, InterruptedException, TimeoutException, DriveException {
        final List<Backup> backups = new ArrayList<>();
        final Drive service = getDriveService();

        final FileList fileList = Tasks.await(listFiles(service), WAIT_TIME_SECS, TimeUnit.SECONDS);
        Log.logD(TAG, "got list of backups");
        final List<File> files = fileList.getFiles();
        for (File file : files) {
            final Backup backup = new Backup(mAppCtxt, file);
            backups.add(backup);
        }

        return backups;
    }

    /**
     * Create a new backup
     * @param filename name of file
     * @param data     contents of file
     * @return new backup
     */
    @NonNull
    Backup createBackup(final String filename, final byte[] data)
            throws ExecutionException, InterruptedException, TimeoutException, DriveException {
        Backup backup;
        final Drive service = getDriveService();

        // create a new file
        File driveFile = Tasks.await(createFile(service, filename, Backup.getAppProperties(mAppCtxt)),
                WAIT_TIME_SECS, TimeUnit.SECONDS);
        if (driveFile == null) {
            final String err = mAppCtxt.getString(R.string.err_create_backup);
            throw new DriveException(err);
        }
        final String fileId = driveFile.getId();

        // add the contents
        driveFile = Tasks.await(saveContents(service, fileId, filename, data), WAIT_TIME_SECS, TimeUnit.SECONDS);

        backup = new Backup(mAppCtxt, driveFile);
        Log.logD(TAG, "created backup");

        return backup;
    }

    /**
     * Update a backup
     * @param fileId file to update
     * @param filename file to update
     * @param data update data
     */
    void updateBackup(@NonNull final String fileId, String filename, @NonNull final byte[] data)
            throws ExecutionException, InterruptedException, TimeoutException, DriveException {
        final Drive service = getDriveService();

        // add the contents
        Tasks.await(saveContents(service, fileId, filename, data), WAIT_TIME_SECS, TimeUnit.SECONDS);
        Log.logD(TAG, "updated backup");
    }

    /**
     * Delete a backup
     * @param fileId name of file
     */
    void deleteBackup(final String fileId)
            throws ExecutionException, InterruptedException, TimeoutException, DriveException {
        final Drive service = getDriveService();

        Tasks.await(deleteFile(service, fileId), WAIT_TIME_SECS, TimeUnit.SECONDS);
        Log.logD(TAG, "deleted backup");
    }

    /**
     * Get the contents of a backup
     * @param fileId file to retrieve
     * @return contents of a backup
     */
    @NonNull
    BackupContents getBackupContents(final String fileId)
            throws ExecutionException, InterruptedException, TimeoutException, DriveException {
        final Drive service = getDriveService();

        final BackupContents backupContents = Tasks.await(getContents(service, fileId), WAIT_TIME_SECS,
                TimeUnit.SECONDS);
        Log.logD(TAG, "got contents of file");

        return backupContents;
    }

    /**
     * Create a zip file in the user's App directory.
     */
    private Task<File> createFile(Drive service, String fileName, Map<String, String> appProperties) {
        return Tasks.call(App.getExecutors().networkIO(), () -> {
            File metadata = new File().setParents(Collections.singletonList(APP_FOLDER)).setMimeType(MIME_TYPE)
                    .setName(fileName).setAppProperties(appProperties);

            final File driveFile = service.files().create(metadata)
                    .setFields("id, name, modifiedByMeTime, appProperties").execute();
            if (driveFile == null) {
                throw new IOException("Null result when requesting file creation.");
            }
            return driveFile;
        });
    }

    /**
     * Get the files in the  user's App Data folder
     */
    private Task<FileList> listFiles(Drive service) {
        return Tasks.call(App.getExecutors().networkIO(), () -> {

            final FileList files = service.files().list().setSpaces("appDataFolder")
                    .setFields("files(id, name, modifiedByMeTime, appProperties)").execute();
            if (files == null) {
                throw new IOException("Null result when requesting file list.");
            }

            return files;
        });
    }

    /**
     * Get the contents of a backup
     */
    private Task<BackupContents> getContents(Drive service, String fileId) {
        return Tasks.call(App.getExecutors().networkIO(), () -> {
            final BackupContents backupContents = new BackupContents();

            BufferedInputStream bis = null;
            try {
                InputStream is = service.files().get(fileId).executeMediaAsInputStream();
                bis = new BufferedInputStream(is);
                BackupHelper.INST(mAppCtxt).extractFromZipFile(bis, backupContents);
            } finally {
                if (bis != null) {
                    bis.close();
                }
            }
            return backupContents;
        });
    }

    /**
     * Seve a file and its contents
     */
    private Task<File> saveContents(Drive service, String fileId, String filename, byte[] contents) {
        return Tasks.call(App.getExecutors().networkIO(), () -> {
            // Create a File containing any metadata changes
            final File metadata = new File().setName(filename);

            // Convert content to an AbstractInputStreamContent instance
            final ByteArrayContent contentStream = new ByteArrayContent(MIME_TYPE, contents);

            // Update the metadata and contents
            return service.files().update(fileId, metadata, contentStream)
                    .setFields("id, name, createdTime, modifiedByMeTime, appProperties").execute();
        });
    }

    /**
     * Delete a file on Drive
     */
    private Task<Void> deleteFile(Drive service, String fileId) {
        return Tasks.call(App.getExecutors().networkIO(), () -> {
            return service.files().delete(fileId).execute();
        });
    }

    /** Check permissions for our appFolder */
    private void checkAppFolderPermission() throws DriveException {
        if (noAppFolderPermission()) {
            throw new DriveException(ERR_PERMISSION);
        }
    }

    /** get the DriveService */
    @NonNull
    private Drive getDriveService() throws DriveException {
        checkAppFolderPermission();
        Drive service = null;
        final HttpTransport httpTransport = new NetHttpTransport();
        final JacksonFactory jsonFactory = new JacksonFactory();
        final String email = User.INST(mAppCtxt).getEmail();
        final Collection<String> scopes = new ArrayList<>(Collections.singletonList(DriveScopes.DRIVE_APPDATA));

        if (!TextUtils.isEmpty(email)) {
            // Use the authenticated account to sign in to the Drive service.
            GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(mAppCtxt, scopes);
            credential.setSelectedAccount(new Account(email, "com.google"));
            service = new Drive.Builder(httpTransport, jsonFactory, credential)
                    .setApplicationName(mAppCtxt.getString(R.string.app_name)).build();
        }
        if (service == null) {
            throw new DriveException(ERR_INTERNAL);
        }

        return service;
    }
}