Integrating Google Drive in Android

Kevin Moore

Integrating Google Drive in Android

There are times when a user is required to choose a file to upload into an app. There are many places from which to upload files: local storage on the device, Dropbox, Google Drive and other services. In this tutorial, you will create an app that will authenticate a user with Google, launch Google Drive, and then allow a user to choose a file. Once the user selects a file, the app will download and open it.

The Google Drive SDK is used to connect to a user’s Google Drive files. This tutorial will focus on allowing a user to choose an existing file, download it and display it through the app. You will use Google Drive’s built-in file picker, which will allow us to choose any file that is on the user’s Google drive.

Note: If you’re new to Android development or the Kotlin language, it’s highly recommended that you start with Beginning Android Development with Kotlin to learn your way around the basic tools and concepts.

Getting Started

Make sure you have Android Studio and the Kotlin plugin installed before you begin. To install Android Studio go to developer.android.com. Your phone or emulator will need up-to-date Google Play services on the device to run the app.

Since your UI will be bare-bones, open up Android Studio 3.1.1 or later and create a new project. From Android Studio, select Start a new Android Studio project from the startup screen or New Project from the File menu.

Enter the name GoogleDriveDemo, your company domain (or example.com if your wish) and a project location. Make sure that Kotlin support is selected and then press Next.

Create a Project - Google Drive Demo

You shouldn’t need to change anything on this screen. Just click Next.

Create a Project - Google Drive Demo

Select Empty Activity and press Next.

Create a Project - Google Drive Demo

Click Finish.

Registering for Google Drive

In order to use the Google Drive SDK, you need to enable the API in your app.

Walking through the steps:

  • Go to Google Console.
  • Sign up for a developer’s account if you don’t have one or sign in.
  • Create a project and click Continue.
  • Create a Project - Google Drive Demo

  • Add a project name and click Create.
  • Credentials

  • Select Library on the left-hand side to go to the Search screen.
  • Search API

  • Type “Google Drive” and select Google Drive API.
  • Drive API

  • Select Enable.
  • Enable API

  • At this point, you may be asked to set a name on the consent screen before you create a credential. Do so and then create your credential.
  • Google Drive API Consent Screen Warning

  • You don’t need to complete the form that appears — only the name is required. Enter a name and press Save:
  • Google Drive API Consent Screen

  • Click Create credentials.
  • Create Credentials

  • Select OAuth Client ID.
  • OAuth Client ID

  • Choose Android, enter a name and the package name that you used to create your app. Although the hint refers to the package name in AndroidManifest.xml, it has to match the applicationId in build.gradle instead — otherwise, the login flow will fail.
  • Create Client ID

  • Copy the keytool text (press the Copy icon) and paste it into a terminal.
  • Change the path-to-debug-or-production-keystore to your default debug keystore location:
    • On Mac or Linux, ~/.android/debug.keystore
    • On Windows, %USERPROFILE%/.android/debug.keystore
  • After you execute the command, you will be prompted to enter the keystore password. The password for the debug keystore is blank by default, so you can just press Return or Enter.

    If everything works correctly, you should see something like this:

  • Command Line

  • Copy the SHA1 value from the terminal into the text field and press Create. The Client ID dialog will appear. Press OK.
  • Client ID

  • Now, you are taken to the Credentials page. Authorization on Android uses the SHA1 fingerprint and package name to identify your app, so you don’t have to download any JSON file or copy any API key or secret to your project.
  • Credentials Page

Building Your Android App

Go back to the Android app and you can start adding settings and code.

Updating the Gradle File

First, you’ll update the build.gradle file inside the app folder. Add two variables for the version of Google Play services and Android support libraries after the apply plugin statements near the top:

ext {
  play_services_version = "15.0.1"
  support_version = "27.1.1"
}

This will let you re-use the variables and easily change the versions. Replace the version of com.android.support:appcompat-v7 with the variable, and add the support design library, both in the dependencies block:

implementation "com.android.support:appcompat-v7:$support_version"
implementation "com.android.support:design:$support_version"

The appcompat library contains all of the compatibility classes that help you write code that works on many versions of the Android OS. The design support library is used in this tutorial to display a Snackbar message.

Next, add the libraries for the Google Drive SDK and the Okio library from Square for downloading files.

// Google Drive
implementation "com.google.android.gms:play-services-auth:$play_services_version"
implementation "com.google.android.gms:play-services-drive:$play_services_version"
implementation 'com.squareup.okio:okio:1.14.0'

Now, sync the project Gradle files (File ▸ Sync Project with Gradle Files)

Modifying the Android Manifest

Next, you’ll modify AndroidManifest.xml. Open the file and add internet permissions right above the tag:

<uses-permission android:name="android.permission.INTERNET"/>

Add the following code to specify the version of Google Play Services as the first sub-element in the tag:

<meta-data
    android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version" />

If you command-click (or Ctrl-click on PC) on the @integer/google_play_services_version you will see that it takes you to the play services values file that let’s the Google Play SDK know which version you are using.

Creating a File Provider

Next, you’ll create a FileProvider. This is required for Android 8.0 Oreo and above to access local files.

First, create a new directory by right-clicking on the app/res directory and selecing New ▸ Android Resource Directory. Name it xml. Right-click on the xml directory and select New ▸ File; name it provider_paths.

This is needed since Android Oreo does not support sharing file:// urls. Open the new file and paste in the following:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
   <external-path name="external_files" path="."/>
</paths>

Now, in the Android Manifest file, after the meta-data tag you recently added, add:

<provider
  android:name="android.support.v4.content.FileProvider"
  android:authorities="${applicationId}.provider"
  android:exported="false"
  android:grantUriPermissions="true">
  <meta-data
    android:name="android.support.FILE_PROVIDER_PATHS"
    android:resource="@xml/provider_paths"/>

This sets up your app to use Android’s FileProvider class to handle local files as urls instead of as files. This is a security restriction that Google has implemented.

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.

Opening a Picked-File Dialog

You have created the methods to handle the result of signing in and picking a file, but you don’t yet have a method to initiate those actions. Create a method named pickFiles() to open the picked-file dialog:

/**
 * Prompts the user to select a text file using OpenFileActivity.
 *
 * @return Task that resolves with the selected item's ID.
 */
fun pickFiles(driveId: DriveId?) {
  val builder = OpenFileActivityOptions.Builder()
  if (config.mimeTypes != null) {
    builder.setMimeType(config.mimeTypes)
  } else {
    builder.setMimeType(documentMimeTypes)
  }
  if (config.activityTitle != null && config.activityTitle.isNotEmpty()) {
    builder.setActivityTitle(config.activityTitle)
  }
  if (driveId != null) {
    builder.setActivityStartFolder(driveId)
  }
  val openOptions = builder.build()
  pickItem(openOptions)
}

You set the mime type and title, and then set the starting folder if driveId is provided. Then call pickItem with those options.

Next add the pickItem method:

private fun pickItem(openOptions: OpenFileActivityOptions) {
  val openTask = driveClient?.newOpenFileActivityIntentSender(openOptions)
  openTask?.let {
    openTask.continueWith { task ->
      ActivityCompat.startIntentSenderForResult(activity, task.result, REQUEST_CODE_OPEN_ITEM,
          null, 0, 0, 0, null)
    }
  }
}

This will start Google Drive’s File Picker activity, which will call your onActivityResult with the user’s response.

Logging In and Out

Next, you add a method that can retrieve any account that has been signed in from previous launches:

fun checkLoginStatus() {
  val requiredScopes = HashSet<Scope>(2)
  requiredScopes.add(Drive.SCOPE_FILE)
  requiredScopes.add(Drive.SCOPE_APPFOLDER)
  signInAccount = GoogleSignIn.getLastSignedInAccount(activity)
  val containsScope = signInAccount?.grantedScopes?.containsAll(requiredScopes)
  val account = signInAccount
  if (account != null && containsScope == true) {
    initializeDriveClient(account)
  }
}

If a signed-in account is found and no scope has changed, you call initializeDriveClient() that you created earlier to handle the sign in. Add the following method to launch the Authentication dialog:

fun auth() {
  activity.startActivityForResult(googleSignInClient.signInIntent, REQUEST_CODE_SIGN_IN)
}

Finally, add a method to allow a user to log out.

fun logout() {
  googleSignInClient.signOut()
  signInAccount = null
}

Updating MainActivity

Now, you will turn your attention back to the MainActivity.
Above the onCreate() function, create a simple enum to keep track of the buttons state:

enum class ButtonState {
  LOGGED_OUT,
  LOGGED_IN
}

As mentioned earlier, the activity needs to be set as a serviceListener so that it can respond to the service. Implement the ServiceListener interface in the MainActivity:

class MainActivity : AppCompatActivity(), ServiceListener {

And add the interface methods:

override fun loggedIn() {
}

override fun fileDownloaded(file: File) {
}

override fun cancelled() {
}

override fun handleError(exception: Exception) {
}

Add properties for the service and button state:

private lateinit var googleDriveService: GoogleDriveService
private var state = ButtonState.LOGGED_OUT

You need to change the state of the buttons based on your logged-in or logged-out state. Consequently, you create a function named setButtons:

private fun setButtons() {
  when (state) {
    ButtonState.LOGGED_OUT -> {
      status.text = getString(R.string.status_logged_out)
      start.isEnabled = false
      logout.isEnabled = false
      login.isEnabled = true
    }

    else -> {
      status.text = getString(R.string.status_logged_in)
      start.isEnabled = true
      logout.isEnabled = true
      login.isEnabled = false
    }
  }
}

status, start, logout, and login are the ID of the views you created in activity_main.xml. You should be able to import them using Option+Return on macOS Alt+Enter on PC as long as you have apply plugin:'kotlin-android-extensions' in the app module build.gradle, which new projects do by default.

Update onCreate() to be:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)

  //1
  val config = GoogleDriveConfig(
      getString(R.string.source_google_drive),
      GoogleDriveService.documentMimeTypes
  )
  googleDriveService = GoogleDriveService(this, config)

  //2
  googleDriveService.serviceListener = this

  //3
  googleDriveService.checkLoginStatus()

  //4
  login.setOnClickListener {
    googleDriveService.auth()
  }
  start.setOnClickListener {
    googleDriveService.pickFiles(null)
  }
  logout.setOnClickListener {
    googleDriveService.logout()
    state = ButtonState.LOGGED_OUT
    setButtons()
  }

  //5
  setButtons()
}

Here’s what the above does:

  1. Creates the service with your title and the document mime types.
  2. Sets MainActivity as the listener.
  3. Changes the state to logged-in if there is any logged-in account present.
  4. Sets the button click listeners. There are three buttons: Login, Pick a File and Logout.
  5. Updates views based on the current state.

Handling the OnActivityResult Method

Add the onActivityResult() method and have it pass the result to the service:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
 googleDriveService.onActivityResult(requestCode, resultCode, data)
}

Now, add implementations for the listener methods:

override fun loggedIn() {
  state = ButtonState.LOGGED_IN
  setButtons()
}

override fun fileDownloaded(file: File) {
  val intent = Intent(Intent.ACTION_VIEW)
  val apkURI = FileProvider.getUriForFile(
      this,
      applicationContext.packageName + ".provider",
      file)
  val uri = Uri.fromFile(file)
  val extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
  val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
  intent.setDataAndType(apkURI, mimeType)
  intent.flags = FLAG_GRANT_READ_URI_PERMISSION
  if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
  } else {
    Snackbar.make(main_layout, R.string.not_open_file, Snackbar.LENGTH_LONG).show()
  }
}

override fun cancelled() {
  Snackbar.make(main_layout, R.string.status_user_cancelled, Snackbar.LENGTH_LONG).show()
  }

override fun handleError(exception: Exception) {
  val errorMessage = getString(R.string.status_error, exception.message)
  Snackbar.make(main_layout, errorMessage, Snackbar.LENGTH_LONG).show()
}

The code inside loggedIn(), cancelled(), and handleError() are pretty straightforward. They update the UI and/or display messages with Snackbar.

In fileDownloaded(), a file is received; subsequently, you want the system to open the file. This is where the FileProvider information you put in the AndroidManifest.xml file comes in.

In Android 8.0 Oreo and above, you can no longer open file:// url’s, so you need to provide your own FileProvider for that. You don’t need any other code than this. MimeTypeMap is a system class that has a few helper methods you can use to get the file extension and mime type from the url. You create an intent and make sure that the system can handle it before starting the activity — the app will crash otherwise.

Time to give it a try! Build and run the app.

First, try logging in:

You will first be presented with an account chooser. After you’ve chosen an account, you’ll need to give the app permissions to access your Google Drive.

Next, hit the “Start Google Drive” button, and you will see your files like this:

Once you select a file and press Select, the download process will start. After the download is complete, you should then see the file you picked automatically open in a system viewer.

Where to Go From Here?

In this tutorial, you have learned how to integrate your app with Google Drive and how to download a file. Congratulations on successfully downloading files from your Google Drive!

You can download the final project by using the download button at the top or bottom of this tutorial.

You can do much more with Google Drive. Try, for example, adding more capabilities to your app, such as creating a file or deleting a file. Check out the documentation about other Google Drive SDK features for Android.

If you have any comments or questions about this tutorial or Google Drive SDK, feel free to join the forum discussion below!

Download Materials

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Kevin Moore

I really enjoy developing Android and iOS applications and in my spare time create apps that I freely share. I have been programming on the Android platform for over 8 years and iOS for a year. I have a course on Kotlin on raywenderlich.com and have done talks at conferences.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 27 total!

iOS Team

... 83 total!

Android Team

... 47 total!

Unity Team

... 16 total!

Articles Team

... 4 total!

Resident Authors Team

... 32 total!

Podcast Team

... 4 total!

Recruitment Team

... 8 total!

Illustration Team

... 4 total!