tisdag 8 december 2020

Implementing Drive API v3 in Android

 Documentation be found at https://developers.google.com/resources/api-libraries/documentation/drive/v3/java/latest/overview-summary.html

API version was Drive API v3 (Rev. 197) 1.25.0 when this was written.


First off we need to solve the authorization and enable the Drive API for the app.

There’s a quickstart guide at https://developers.google.com/drive/api/v3/enable-drive-api for how to enable it.


Part 1 - Authorization

First we need to implement these three libraries inside the project.

compile 'com.google.android.gms:play-services-auth:19.0.0'

compile 'com.google.api-client:google-api-client:1.22.0'

compile 'com.google.api-client:google-api-client-android:1.22.0'


I made a simple SignInHelper.class to help with the authorization request.

SignInHelper.class


import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInClient;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes;
import com.google.android.gms.common.api.Scope;
import com.google.api.services.drive.DriveScopes;

public class SignInHelper {
    public static final int REQUEST_CODE_SIGN_IN = 20;

    private GoogleSignInAccount account;
    private GoogleSignInClient client;


    public SignInHelper(Context context)
    {
        this.account = GoogleSignIn.getLastSignedInAccount(context);
    }
    // More info about the DriveScopes at
    // https://developers.google.com/resources/api-libraries/documentation/drive/v3/java/latest/com/google/api/services/drive/DriveScopes.html

    public boolean isSignedIn() {
        return (account != null && account.getGrantedScopes().contains(DriveScopes.DRIVE_FILE));
    }

    public Intent startSignIn(Context context) {
        buildGoogleSignInClient(context);
        return client.getSignInIntent();
    }

    private void buildGoogleSignInClient(Context context) {
        GoogleSignInOptions signInOptions =
                new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                        .requestEmail()
                        .requestScopes(new Scope(DriveScopes.DRIVE_FILE))
                        .build();

        client =  GoogleSignIn.getClient(context, signInOptions);
    }

    public GoogleSignInAccount getAccount() {
        return account;
    }

    public void setAccount(GoogleSignInAccount account)
    {
        this.account = account;
    }

    public void parseErrorMessage(String message, Context context) {
        int errorCode = Integer.parseInt(message.replaceAll("[^\\d]", ""));
        switch (errorCode) {
            case GoogleSignInStatusCodes.SIGN_IN_CANCELLED:
                Toast.makeText(context, "No account selected.", Toast.LENGTH_LONG).show();
                break;
            case GoogleSignInStatusCodes.SIGN_IN_FAILED:
                Toast.makeText(context, "Login failed. Application authorized?", Toast.LENGTH_LONG).show();
                break;
            default:
                Toast.makeText(context, "Error: " + message, Toast.LENGTH_LONG).show();
        }
    }
}

Next we need a class that can talk to the Drive API, Google has made a simple one at

I'll shorten it for this blog post, only function will be to send an array of bytes.
DriveServiceHelper.java
package com.elluid.drivesample.drive;

/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import android.app.Activity;
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.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.File;

import java.util.Collections;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class DriveServiceHelper {
    private final Executor mExecutor = Executors.newSingleThreadExecutor();
    private final Drive mDriveService;

    private DriveServiceHelper(Drive driveService) {
        mDriveService = driveService;
    }

    public Task<String> sendFile(String name, byte[] bytes) {
        return Tasks.call(mExecutor, () -> {
            File metadata = new File().setName(name);
            metadata.setName(name);
            ByteArrayContent contentStream = new ByteArrayContent("text/plain", bytes);
            mDriveService.files().create(metadata, contentStream).execute();
            return name;
        });
    }

    public static DriveServiceHelper getDriveServiceHelper(GoogleSignInAccount googleSignInAccount, Activity activity) {
        GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(
                activity, Collections.singleton(DriveScopes.DRIVE_FILE));
        credential.setSelectedAccount(googleSignInAccount.getAccount());
        Drive googleDriveService = new Drive.Builder(new NetHttpTransport.Builder().build(),
                new GsonFactory(), credential)
                .setApplicationName("Drive Sample")
                .build();
        return new DriveServiceHelper(googleDriveService);
    }
}
DRIVE_FILE will only give permission to manage and view the files created by the app.

Authorization and a simple Drive API class ready to be used inside an activity. Lets start with a simple layout with a 'Connect' button, a textview for Drive Status and another button to send some sample data.

activity_main.xml
Paste your text here.<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/drive_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        style="@style/TextAppearance.AppCompat.Headline"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <Button
        android:id="@+id/button_send_data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send sample data."
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/drive_status"/>

    <Button
        android:id="@+id/button_connect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Connect to Drive"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
And finally the activity that uses it all. 
MainActivity.java
package com.elluid.drivesample;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.elluid.drivesample.drive.DriveServiceHelper;
import com.elluid.drivesample.drive.SignInHelper;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import static com.elluid.drivesample.drive.SignInHelper.REQUEST_CODE_SIGN_IN;

public class MainActivity extends AppCompatActivity {

    private SignInHelper signInHelper;
    private TextView textviewDriveStatus;
    private Button buttonSendData;

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode)
        {
            case REQUEST_CODE_SIGN_IN:
                handleSignInResult(data);
                break;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textviewDriveStatus = findViewById(R.id.drive_status);
        buttonSendData = findViewById(R.id.button_send_data);
        findViewById(R.id.button_connect).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                signIn();
            }
        });

        buttonSendData.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                sendSampleData();
            }
        });
    }

    private void signIn() {
        if(signInHelper == null)
            signInHelper = new SignInHelper(MainActivity.this);
        if(signInHelper.isSignedIn()) {
            updateDriveStatus();
        } else {
            Intent startSignIn = signInHelper.startSignIn(this);
            startActivityForResult(startSignIn, REQUEST_CODE_SIGN_IN);
        }
    }

    private void handleSignInResult(Intent result) {

        GoogleSignIn.getSignedInAccountFromIntent(result).addOnSuccessListener(new OnSuccessListener<GoogleSignInAccount>() {
                    @Override
                    public void onSuccess(GoogleSignInAccount googleSignInAccount) {
                        signInHelper.setAccount(googleSignInAccount);
                        updateDriveStatus();
                    }
                })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        signInHelper.parseErrorMessage(e.getMessage(), MainActivity.this);
                    }
                });
    }

    private void updateDriveStatus()
    {
        textviewDriveStatus.setText("Authorized for Drive usage!");
        textviewDriveStatus.setVisibility(View.VISIBLE);
        buttonSendData.setVisibility(View.VISIBLE);
    }

    private void sendSampleData()
    {
        DriveServiceHelper driveServiceHelper = DriveServiceHelper.getDriveServiceHelper(signInHelper.getAccount(), this);
        String testData = "Hi Drive! \n I Hope this arrives safe and sound. \n Regards, Elluid";
        Task<String> uploadTask = driveServiceHelper.sendFile("sample.txt", testData.getBytes());

        uploadTask.addOnCompleteListener(new OnCompleteListener<String>() {
            @Override
            public void onComplete(@NonNull Task<String> task) {
                if(task.isSuccessful()) {
                    Toast.makeText(getBaseContext(), task.getResult() + " sent successfully.", Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(getBaseContext(), "Error:" + task.getException().getMessage(), Toast.LENGTH_LONG).show();
                }
            }
        });
    }
}
Had to rewrite mine recently as i was using v2. Correct me or ask question in the comments and i need to continue my Kotlin route. Still feels more normal to use Java, so many different special ways to handle things in Kotlin but i'm trying.. 

Hope it helps someone!

Full project can be found at https://github.com/elluid-data/drive-sample.