Adding Dropbox to an Android App

Valdio Veliu
Share

This article was peer reviewed by Wern Ancheta. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Document and file storage has been essential to computing devices since their inception. Traditionally storage was local, but the past years have seen an increasing move to storing in the cloud. Cloud storage has it’s problems, but offers near infinite capacity and low cost.

In this tutorial I will explain how to store files from an Android app in Dropbox by using the Dropbox Core SDK for Java 6+ in Android. According to the SDK description this Core SDK is a Java library to access Dropbox’s HTTP-based Core API v2. This SDK also supports the older Core API v1, but that support will be removed at some point.

You can find the final code for this project on GitHub.

To connect with Dropbox, you need to have a dropbox account and use the Dropbox API. Create a new app in the App Console.

Create App

After the new app is created it will be set to development status. With this status the app can only be used by test users and if you Enable Additional Users these can be up to 100. When the app is ready you have to apply for production status.

The app console contains app info and you will need the App Key for the Android project.

App Console

Add Permissions

Create a project in Android Studio (With a Login Activity) and in AndroidManifest.xml add the following for internet and storage permissions. You can remove the auto-generated permissions added in AndroidManifest.xml by the LoginActivity. They are not required in this project.

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

Add Dependencies

In build.gradle(Module: app) add the following:

dependencies {
    //...
    compile 'com.dropbox.core:dropbox-core-sdk:2.0.1'
    compile 'com.squareup.picasso:picasso:2.5.2'
}

Note: The first dependency is for the Dropbox Core API. The Picasso library is not mandatory, but is useful for loading images.

Add the Dropbox Activity

Open AndroidManifest.xml, and in the <application></application> section, add the AuthActivity.

<application>
...
    <!-- Dropbox AuthActivity -->
    <activity
        android:name="com.dropbox.core.android.AuthActivity"
        android:configChanges="orientation|keyboard"
        android:launchMode="singleTask">
        <intent-filter>

            <!-- Insert your app key after “db-  ...” -->
            <data android:scheme="db-APP_KEY" />

            <action android:name="android.intent.action.VIEW" />

            <category android:name="android.intent.category.BROWSABLE" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
</application>

Duplicate File Error

While testing the app I encountered an error that after reading this Stackoverflow answer was due to a conflict during build time of multiple License.txt or Notice.txt files. To avoid this conflict, in build.gradle(Module: app), in the android{…} section, exclude these files.

android {
...

  packagingOptions {
      exclude 'META-INF/LICENSE'
      exclude 'META-INF/LICENSE.txt'
      exclude 'META-INF/NOTICE'
      exclude 'META-INF/NOTICE.txt'
  }
}

App key

Add your app key to strings.xml:

<!-- Change this to your app key -->
<string name="APP_KEY">APP_KEY_HERE</string>

Now the Android project is configured to use the Dropbox API, add a new Activity to the project, with the File->New->Activity->Basic Activity menu item. Call it MainActivity.java.

Project Description

The sample app will consist of logging in to the user’s dropbox account, getting detailed information and performing a image upload to the app’s Dropbox folder. The project will have two activities, LoginActivity and MainActivity.

Login with Dropbox

The LoginActivity‘s task is straightforward. On a button click it performs a login task to generate an access token for the Dropbox app identified by the APP_KEY in the string resources.

The startOAuth2Authentication() method will open the Dropbox AuthActivity, this is the activity added to the Manifest file. In the AuthActivity the user must confirm their Dropbox account. After the user has confirmed their Dropbox account, redirect them to the LoginActivity.

The startOAuth2Authentication() method doesn’t return any access Token, it only opens AuthActivity which is internally used for authentication. Now the user is authenticated, they need an access token.

AuthActivity

AuthActivity

In LoginActivity replace all the auto-generated code, except the project package name, with the following:

public class LoginActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        Button SignInButton = (Button) findViewById(R.id.sign_in_button);
        SignInButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                Auth.startOAuth2Authentication(getApplicationContext(), getString(R.string.APP_KEY));
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        getAccessToken();
    }

    public void getAccessToken() {
        String accessToken = Auth.getOAuth2Token(); //generate Access Token
        if (accessToken != null) {
            //Store accessToken in SharedPreferences
            SharedPreferences prefs = getSharedPreferences("com.example.valdio.dropboxintegration", Context.MODE_PRIVATE);
            prefs.edit().putString("access-token", accessToken).apply();

            //Proceed to MainActivity
            Intent intent = new Intent(LoginActivity.this, MainActivity.class);
            startActivity(intent);
        }
    }
}

For the layout of this activity, update activity_login.xml to:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.valdio.dropboxintegration.LoginActivity">
    <!-- Update package name -->

    <Button
        android:id="@+id/sign_in_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/sign_in"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

Add the following to strings.xml for the sign in button text:

...
<string name="sign_in">Sign in</string>

The LoginActivity‘s onResume() lifecycle method requests the token by calling the getOAuth2Token() method. The token is stored in the app’s SharedPreferences data for later use, then MainActivity opened.

The onResume() lifecycle method was called before when the app opened for the first time. Why didn’t you get a token then?

This is because the user was not authenticated before. When redirecting from the AuthActivity with successful authentication, the onResume() method will be called once again and generate the access token.

Dropbox Client Is Now Logged In

In MainActivity declare the following variables:

public class MainActivity extends AppCompatActivity {

    private static final int IMAGE_REQUEST_CODE = 101;
    private String ACCESS_TOKEN;
    ...
}

In MainActivity‘s content_main.xml layout file replace the content with the following:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="valdioveliu.valdio.com.dropboxintegration.MainActivity"
    tools:showIn="@layout/activity_main">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:id="@+id/accountData">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true" />

        <TextView
            android:id="@+id/textView3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/imageView"
            android:layout_marginLeft="15dp"
            android:layout_marginTop="50dp"
            android:text="Name"
            android:textAppearance="?android:attr/textAppearanceLarge" />

        <TextView
            android:id="@+id/name_textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:gravity="center_horizontal"
            android:text=""
            android:textAppearance="?android:attr/textAppearanceLarge" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/textView3"
            android:layout_marginLeft="15dp"
            android:layout_marginTop="58dp"
            android:text="Email"
            android:textAppearance="?android:attr/textAppearanceLarge" />

        <TextView
            android:id="@+id/email_textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_below="@+id/textView"
            android:gravity="center_horizontal"
            android:text=""
            android:textAppearance="?android:attr/textAppearanceLarge" />
    </LinearLayout>
</RelativeLayout>

Account Information

Getting user account information or accessing Dropbox in any way is a network request, and with Android, network requests are handled asynchronously. These network requests all require a Dropbox client.

Create DropboxClient.java and add the following code:

public class DropboxClient {

    public static DbxClientV2 getClient(String ACCESS_TOKEN) {
        // Create Dropbox client
        DbxRequestConfig config = new DbxRequestConfig("dropbox/sample-app", "en_US");
        DbxClientV2 client = new DbxClientV2(config, ACCESS_TOKEN);
        return client;
    }
}

The ACCESS_TOKEN string in the getClient() method is the token received during the user’s login.

The next task is getting the user’s account details. The UserAccountTask class represents the accounts details request. The constructor of this asynchronous class has an interface parameter of TaskDelegate used to return the accounts info back to the activity in which the task was executed.

Create UserAccountTask.java and add the following code:

public class UserAccountTask extends AsyncTask<Void, Void, FullAccount>  {

    private DbxClientV2 dbxClient;
    private TaskDelegate  delegate;
    private Exception error;

    public interface TaskDelegate {
        void onAccountReceived(FullAccount account);
        void onError(Exception error);
    }

    UserAccountTask(DbxClientV2 dbxClient, TaskDelegate delegate){
        this.dbxClient =dbxClient;
        this.delegate = delegate;
    }

    @Override
    protected FullAccount doInBackground(Void... params) {
        try {
            //get the users FullAccount
            return dbxClient.users().getCurrentAccount();
        } catch (DbxException e) {
            e.printStackTrace();
            error = e;
        }
        return null;
    }

    @Override
    protected void onPostExecute(FullAccount account) {
        super.onPostExecute(account);

        if (account != null && error == null){
            //User Account received successfully
            delegate.onAccountReceived(account);
        }
        else {
            // Something went wrong
            delegate.onError(error);
        }
    }
}

The getUserAccount method is an example of how the UserAcountTask class is executed in the MainActivity class.

Add the following method to the MainActivity class, you will use it later:

protected void getUserAccount() {
    if (ACCESS_TOKEN == null)return;
    new UserAccountTask(DropboxClient.getClient(ACCESS_TOKEN), new UserAccountTask.TaskDelegate() {
        @Override
        public void onAccountReceived(FullAccount account) {
            //Print account's info
            Log.d("User", account.getEmail());
            Log.d("User", account.getName().getDisplayName());
            Log.d("User", account.getAccountType().name());
            updateUI(account);
        }
        @Override
        public void onError(Exception error) {
            Log.d("User", "Error receiving account details.");
        }
    }).execute();
}

To display the user’s account details on the UI add the following function to MainActivity class:

private void updateUI(FullAccount account) {
  ImageView profile = (ImageView) findViewById(R.id.imageView);
  TextView name = (TextView) findViewById(R.id.name_textView);
  TextView email = (TextView) findViewById(R.id.email_textView);

  name.setText(account.getName().getDisplayName());
  email.setText(account.getEmail());
  Picasso.with(this)
          .load(account.getProfilePhotoUrl())
          .resize(200, 200)
          .into(profile);
}

Uploading to Dropbox

Upload tasks happen with the help of InputStream. A specified file is converted to InputStream and with the help of the Core SDK, the stream uploaded to the apps folder in Dropbox.

Create UploadTask.java and add the following code to it:

public class UploadTask extends AsyncTask {

    private DbxClientV2 dbxClient;
    private File file;
    private Context context;

    UploadTask(DbxClientV2 dbxClient, File file, Context context) {
        this.dbxClient = dbxClient;
        this.file = file;
        this.context = context;
    }

    @Override
    protected Object doInBackground(Object[] params) {
        try {
            // Upload to Dropbox
            InputStream inputStream = new FileInputStream(file);
            dbxClient.files().uploadBuilder("/" + file.getName()) //Path in the user's Dropbox to save the file.
                    .withMode(WriteMode.OVERWRITE) //always overwrite existing file
                    .uploadAndFinish(inputStream);
            Log.d("Upload Status", "Success");
        } catch (DbxException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Object o) {
        super.onPostExecute(o);
        Toast.makeText(context, "Image uploaded successfully", Toast.LENGTH_SHORT).show();
    }
}

Note: Android Studio will prompt you about conflicting imports, you need to import java.io.File for the File variable.

To perform an upload you first need something to upload, e.g. an image. You can get an image by starting another activity for receiving a result.

Add the following method to MainActivity:

private void upload() {
  if (ACCESS_TOKEN == null)return;
  //Select image to upload
  Intent intent = new Intent();
  intent.setType("image/*");
  intent.setAction(Intent.ACTION_GET_CONTENT);
  intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
  startActivityForResult(Intent.createChooser(intent,
          "Upload to Dropbox"), IMAGE_REQUEST_CODE);
}

When calling startActivityForResult(), you need to implement the onActivityResult() method to handle results on MainActivity. When a valid image URI is received, construct a file and execute UploadTask.

Add the following method to MainActivity:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if (resultCode != RESULT_OK || data == null) return;
  // Check which request we're responding to
  if (requestCode == IMAGE_REQUEST_CODE) {
      // Make sure the request was successful
      if (resultCode == RESULT_OK) {
          //Image URI received
          File file = new File(URI_to_Path.getPath(getApplication(), data.getData()));
          if (file != null) {
              //Initialize UploadTask
              new UploadTask(DropboxClient.getClient(ACCESS_TOKEN), file, getApplicationContext()).execute();
          }
      }
  }
}

In the onActivityResult() method is the parameter, URI_to_Path.getPath(getApplication(), data.getData()). The URI_to_Path java class is a helper class to convert file URIs into absolute paths. It’s based on this StackOverflow answer.

This is a large class and not directly related to this tutorial, so create URI_to_Path.java and add the code here to it. Make sure to change the package name to yours.

Finally comes the onCreate() method. Add the following code to MainActivity, replacing anything that already exists:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    if (!tokenExists()) {
        //No token
        //Back to LoginActivity
        Intent intent = new Intent(MainActivity.this, LoginActivity.class);
        startActivity(intent);
    }

    ACCESS_TOKEN = retrieveAccessToken();
    getUserAccount();

    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            upload();
        }
    });
}

Every time the app opens you need to check if there is an access token. If the token exists, retrieve it from SharedPreferences. Add these methods to MainActivity to help with that:

private boolean tokenExists() {
    SharedPreferences prefs = getSharedPreferences("com.example.valdio.dropboxintegration", Context.MODE_PRIVATE);
    String accessToken = prefs.getString("access-token", null);
    return accessToken != null;
}

private String retrieveAccessToken() {
    //check if ACCESS_TOKEN is stored on previous app launches
    SharedPreferences prefs = getSharedPreferences("com.example.valdio.dropboxintegration", Context.MODE_PRIVATE);
    String accessToken = prefs.getString("access-token", null);
    if (accessToken == null) {
        Log.d("AccessToken Status", "No token found");
        return null;
    } else {
        //accessToken already exists
        Log.d("AccessToken Status", "Token exists");
        return accessToken;
    }
}

Conclusion

That’s all for the basics of Dropbox integration with the v2 API in Android. If you want to take a deeper look at this topic, read the Dropbox for Java documentation, the SDK source and examples.

CSS Master, 3rd Edition