Integrating Google Drive in Android

See how to integrate the Google Drive SDK in order to let your users access and download their Drive files directly to your app. By Kevin D Moore.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Adding Strings

Now, you’ll add the strings that you’ll need for the UI. Open the strings.xml file and add:

<string name="source_google_drive">Google Drive</string>
<string name="start_drive">Start Google Drive</string>
<string name="login">Log In</string>
<string name="logout">Log Out</string>
<string name="status_logged_out">Logged Out</string>
<string name="status_logged_in">Logged In</string>
<string name="status_user_cancelled">User Cancelled</string>
<string name="status_error">We found a problem: %1$s</string>
<string name="not_open_file">Could not open file</string>

The first string is for the Google Drive’s activity title, and the rest are for the UI.

Updating the UI

Next, you’ll update the UI. To do so, you’ll simply create three buttons to Login, Logout, and Open Google Drive, and a TextView to display login status. Open activity_main.xml and replace the contents with the following:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:id="@+id/main_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <Button
    android:id="@+id/login"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/login"
    app:layout_constraintBottom_toTopOf="@+id/start"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <Button
    android:id="@+id/start"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/start_drive"
    app:layout_constraintBottom_toTopOf="@+id/logout"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/login" />

  <Button
    android:id="@+id/logout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/logout"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/start" />

  <TextView
    android:id="@+id/status"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginBottom="8dp"
    android:gravity="center_horizontal"
    android:text="@string/status_logged_out"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>

Run the app and make sure the UI is displayed correctly:

If everything works correctly, you should have a basic UI with three buttons and a status message at the bottom. If the project does not compile or something goes wrong when running, compare your work with each of the steps above.

Creating a ServiceListener Interface

Since there are only a few classes, you will put all of them in the root source folder. Start with the interface that the listener of your service must implement. Create a new Kotlin interface named ServiceListener:

interface ServiceListener {
  fun loggedIn() //1
  fun fileDownloaded(file: File) //2
  fun cancelled() //3
  fun handleError(exception: Exception) //4
}

You may need to choose Option+Return on macOS Alt+Enter on PC to pull in the import for the File class.

These methods notify the listener when:

  1. loggedIn(): A user is successfully authenticated.
  2. fileDownloaded(file: File): A file is selected and downloaded successfully.
  3. cancelled(): A login or file selection is cancelled.
  4. handleError(exception: Exception): There is any error.

This interface will be implemented by MainActivity and used by a service as a way to let the user of the service know when something has happened.

Creating a Data Class: GoogleDriveConfig

Next, create a simple data class for holding the information that the service needs. Create a new data class named GoogleDriveConfig:

data class GoogleDriveConfig(val activityTitle: String? = null, val mimeTypes: List<String>? = null)

This class contains the title that Google Drive will designate as the activity’s title and the mimeTypes that determines which file types to show.

Creating the GoogleDriveService

Next, you’ll create the actual service. Create a new class named GoogleDriveService:

class GoogleDriveService(private val activity: Activity, private val config: GoogleDriveConfig) {

}

The class is not an Android Service, but instead acts as a service for MainActivity. You will be adding the following code, in order.

First, add a companion object:

companion object {
  private val SCOPES = setOf<Scope>(Drive.SCOPE_FILE, Drive.SCOPE_APPFOLDER)
  val documentMimeTypes = arrayListOf(
      "application/pdf", 
      "application/msword", 
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document")

  const val REQUEST_CODE_OPEN_ITEM = 100
  const val REQUEST_CODE_SIGN_IN = 101
  const val TAG = "GoogleDriveService"
}

Scopes are Google Drive’s set of permissions. Therefore, by giving a file and an app folder scope, you tell Google Drive to let you handle files and folders.

The mime types are for the type of files you want to allow the user to pick. If you want the user to choose images, you would use image/*. Here, you pick .pdf and .doc/.docx files.

You also have two request codes to use for handling the result of signing in and picking a file. The TAG constant is used for Logging.

After the companion object section, add the following variables:

var serviceListener: ServiceListener? = null //1
private var driveClient: DriveClient? = null //2
private var driveResourceClient: DriveResourceClient? = null //3
private var signInAccount: GoogleSignInAccount? = null //4

These are:

  1. serviceListener is the listener of your service.
  2. driveClient handles high-level drive functions like Create File, Open File, and Sync.
  3. driveResourceClient handles access to Drive resources and/or files.
  4. signInAccount keeps track of the currently signed-in account.

Now add a GoogleSignInClient property that is lazily-initialized:

private val googleSignInClient: GoogleSignInClient by lazy {
  val builder = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
  for (scope in SCOPES) {
    builder.requestScopes(scope)
  }
  val signInOptions = builder.build()
  GoogleSignIn.getClient(activity, signInOptions)
}

googleSignInClient is created when needed and includes the scopes defined earlier. The last statement returns the GoogleSignInClient.

Handling Activity Results

You need to be able to handle the results from the user who is signing in and picking a file in the MainActivity. Create a method named onActivityResult, which will be called inside onActivityResult of MainActivity:

fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  when (requestCode) {
    REQUEST_CODE_SIGN_IN -> {
      if (data != null) {
        handleSignIn(data)
      } else {
        serviceListener?.cancelled()
      }
    }

    REQUEST_CODE_OPEN_ITEM -> {
      if (data != null) {
        openItem(data)
      } else {
        serviceListener?.cancelled()
      }
    }
  }
}

In the method, you call helper methods or the serviceListener depending on the requestCode. You can check the result against the presence of data instead of resultCode. If no data is returned, it means the user cancelled the action.

Now add the helper method for handling sign in with another method to initialize the drive client:

private fun handleSignIn(data: Intent) {
  val getAccountTask = GoogleSignIn.getSignedInAccountFromIntent(data)
  if (getAccountTask.isSuccessful) {
    initializeDriveClient(getAccountTask.result)
  } else {
    serviceListener?.handleError(Exception("Sign-in failed.", getAccountTask.exception))
  }
}

private fun initializeDriveClient(signInAccount: GoogleSignInAccount) {
  driveClient = Drive.getDriveClient(activity.applicationContext, signInAccount)
  driveResourceClient = Drive.getDriveResourceClient(activity.applicationContext, signInAccount)
  serviceListener?.loggedIn()
}

Once the user has signed in, you handle the result in initializeDriveClient(). This will create your drive clients. It also notifies the listener that the user has successfully signed in.

After a user has picked a file, you will get an activity intent and pass it to openItem(), so add that helper method now:

private fun openItem(data: Intent) {
  val driveId = data.getParcelableExtra<DriveId>(OpenFileActivityOptions.EXTRA_RESPONSE_DRIVE_ID)
  downloadFile(driveId)
}

This function gets the driveId from the intent options and passes that ID to another helper method downloadFile().

The key aspect of the whole service is downloading the picked file. To do that, you need to get an input stream to the file and save it to a local file. You will use Square’s Okio library to easily take that stream and save it to a file.

Add the downloadFile() method now:

private fun downloadFile(data: DriveId?) {
  if (data == null) {
    Log.e(TAG, "downloadFile data is null")
    return
  }
  val drive = data.asDriveFile()
  var fileName = "test"
  driveResourceClient?.getMetadata(drive)?.addOnSuccessListener {
    fileName = it.originalFilename
  }
  val openFileTask = driveResourceClient?.openFile(drive, DriveFile.MODE_READ_ONLY)
  openFileTask?.continueWithTask { task ->
    val contents = task.result
    contents.inputStream.use {
      try {
        //This is the app's download directory, not the phones
        val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
        val tempFile = File(storageDir, fileName)
        tempFile.createNewFile()
        val sink = Okio.buffer(Okio.sink(tempFile))
        sink.writeAll(Okio.source(it))
        sink.close()

        serviceListener?.fileDownloaded(tempFile)
      } catch (e: IOException) {
        Log.e(TAG, "Problems saving file", e)
        serviceListener?.handleError(e)
      }
    }
    driveResourceClient?.discardContents(contents)
  }?.addOnFailureListener { e ->
    // Handle failure
    Log.e(TAG, "Unable to read contents", e)
    serviceListener?.handleError(e)
  }
}

There’s a lot going on in this method. Notice the getMetaData() call. That is needed to get the name of the chosen file. You are then saving the file to your app’s internal download folder (which is not visible to the user), then alerting the listener about the downloaded file and where to find it.