Wednesday, January 21, 2015

Dropbox Upload API on Android

While working on my updater app (or now tentatively known as PART Store), I've come to the conclusion that there is definitely a need for Dropbox file upload. The PART applications deployed in the field has too many issues with RedCAP data upload and it's often necessary to deliver a saved state of the tablets to my local machine to debug what the issues are. I've decided to implement a one-touch button that will upload files to Dropbox using the Dropbox API for Android.

My hardware setup:

1. Xubuntu 14.10 workstation
2. Android Studio (version 1.0.1)
3. Nexus 4 Mobile Phone

Before going on, you will need to visit Dropbox Developer > App Console (DDAC) and follow the wizard to create a new app and get your App Key / App Secret pairs.

Next download the SDK from the Dropbox Developer website and extract the "lib" directory into the "libs" folder of your project. You need to tell Gradle to link to this library so add in dependency compiles to your gradle script so your build.gradle looks like the following (making changes to the dropbox sdk version number. If you're using Eclipse, there are tutorials on how to add jar file libraries so I won't post that here.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile files('libs/dropbox/dropbox-android-sdk-1.6.3.jar')
    compile files('libs/dropbox/json_simple-1.1.jar')
}

Also, it is crucial not to forget to add in an extra activity tag in the AndroidManifest.xml if you are prompting the user to log into their Dropbox where you will have to replace INSERT_APP_KEY with your own app key generated on the DDAC.

<activity
  android:name="com.dropbox.client2.android.AuthActivity"
  android:launchMode="singleTask"
  android:configChanges="orientation|keyboard">
  <intent-filter>
    <!-- Change this to be db- followed by your app key -->
    <data android:scheme="db-INSERT_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>

Furthermore, as you are using internet to upload/download files from Dropbox, you will need internet permission in the manifest.

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

First thing to note is that at the time of writing this post, the tutorial documentation on the Dropbox API hasn't been updated to match the API of version 1.6.3. The bit where they declare some of the initial variables and you get an error that there is no AccessType.

final static private String APP_KEY = "INSERT_APP_KEY";
final static private String APP_SECRET = "INSERT_APP_SECRET";
final static private AccessType ACCESS_TYPE = AccessType.INSERT_APP_ACCESS_TYPE;

Looking at the sample codes, they must've updated the API such that the AccessType is chosen by the app rather than in-line through code as I remembered getting the option for the type of access the app will get when I was setting a up a new "app" in the DDAC. So we'll just remove the AccessType line above and fill in your app key and secret obtained from the wizard when you're creating a new app on the DDAC. Then in my OnCreate code, I've added the following:

AppKeyPair appKeys = new AppKeyPair(APP_KEY, APP_SECRET);
AndroidAuthSession session = new AndroidAuthSession(appKeys);
mDBApi = new DropboxAPI<androidauthsession>(session);

mDBApi.getSession().setOAuth2AccessToken("__my__access__token__");

This creates the DropboxAPI object mDBApi and this is what eventually allows you to do things to the Dropbox account. Now normally, if you were to develop an application that allows users to input their own Dropbox account information, instead of the last line, you would need something like:

// MyActivity below should be your activity class name
mDBApi.getSession().startOAuth2Authentication(MyActivity.this);

This should be implemented in user-action function that prompts the user to log into their dropbox account and allow their dropbox to use your app to access their files. And when your app returns, you'll get the access token (see more on the Dropbox Tutorial website). However, in my case, the subjects do not have Dropbox and I would like all the apps to use one single dropbox account that has files shared without ever logging in. So I will need to hard-code my access token by using getSession().setOAuth2AccessToken(ACCESS_TOKEN).

Now that's done, we'll need to add in a class that does the file uploading. This class has to extend the AsyncTask as Android doesn't allow you to perform network actions on the UI thread. Makes sense, you don't want a file download to halt your app simply because of a slow internet connection. Here is one basically taken from the UploadPicture.java example code provided by DropBox and renamed to UploadFile rather than UploadPicture

/*
 * Copyright (c) 2011 Dropbox, Inc.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package your.org.name;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.AsyncTask;
import android.widget.Toast;

import com.dropbox.client2.DropboxAPI;
import com.dropbox.client2.DropboxAPI.UploadRequest;
import com.dropbox.client2.ProgressListener;
import com.dropbox.client2.exception.DropboxException;
import com.dropbox.client2.exception.DropboxFileSizeException;
import com.dropbox.client2.exception.DropboxIOException;
import com.dropbox.client2.exception.DropboxParseException;
import com.dropbox.client2.exception.DropboxPartialFileException;
import com.dropbox.client2.exception.DropboxServerException;
import com.dropbox.client2.exception.DropboxUnlinkedException;

/**
 * Here we show uploading a file in a background thread, trying to show typical
 * exception handling and flow of control for an app that uploads a file from
 * Dropbox.
 */
public class UploadFile extends AsyncTask<Void, Long, Boolean> {

    private DropboxAPI<?> mApi;
    private String mPath;
    private File mFile;

    private long mFileLen;
    private UploadRequest mRequest;
    private Context mContext;
    private ProgressDialog mDialog;
    final static private String ACCOUNT_PREFS_NAME = "prefs";

    private String mErrorMsg;


    public UploadFile(Context context, DropboxAPI<?> api, String dropboxPath,
                      File file) {
        mContext = context;

        mFileLen = file.length();
        mApi = api;
        mPath = dropboxPath;
        mFile = file;

        mDialog = new ProgressDialog(context);
        mDialog.setMax(100);
        mDialog.setMessage("Uploading " + file.getName());
        mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mDialog.setProgress(0);
        mDialog.setButton("Cancel", new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                // This will cancel the putFile operation
                mRequest.abort();
            }
        });
        mDialog.show();
    }


    @Override
    protected Boolean doInBackground(Void... params) {
        try {
            // By creating a request, we get a handle to the putFile operation,
            // so we can cancel it later if we want to
            FileInputStream fis = new FileInputStream(mFile);
            String path = mPath + mFile.getName();
            mRequest = mApi.putFileOverwriteRequest(path, fis, mFile.length(),
                    new ProgressListener() {
                        @Override
                        public long progressInterval() {
                            // Update the progress bar every half-second or so
                            return 500;
                        }

                        @Override
                        public void onProgress(long bytes, long total) {
                            publishProgress(bytes);
                        }
                    });

            if (mRequest != null) {
                mRequest.upload();
                return true;
            }

        } catch (DropboxUnlinkedException e) {
            // This session wasn't authenticated properly or user unlinked
            mErrorMsg = "This app wasn't authenticated properly.";
        } catch (DropboxFileSizeException e) {
            // File size too big to upload via the API
            mErrorMsg = "This file is too big to upload";
        } catch (DropboxPartialFileException e) {
            // We canceled the operation
            mErrorMsg = "Upload canceled";
        } catch (DropboxServerException e) {
            // Server-side exception. These are examples of what could happen,
            // but we don't do anything special with them here.
            if (e.error == DropboxServerException._401_UNAUTHORIZED) {
                // Unauthorized, so we should unlink them. You may want to
                // automatically log the user out in this case.
            } else if (e.error == DropboxServerException._403_FORBIDDEN) {
                // Not allowed to access this
            } else if (e.error == DropboxServerException._404_NOT_FOUND) {
                // path not found (or if it was the thumbnail, can't be
                // thumbnailed)
            } else if (e.error == DropboxServerException._507_INSUFFICIENT_STORAGE) {
                // user is over quota
            } else {
                // Something else
            }
            // This gets the Dropbox error, translated into the user's language
            mErrorMsg = e.body.userError;
            if (mErrorMsg == null) {
                mErrorMsg = e.body.error;
            }
        } catch (DropboxIOException e) {
            e.printStackTrace();
            // Happens all the time, probably want to retry automatically.
            mErrorMsg = "Network error.  Try again.";
        } catch (DropboxParseException e) {
            // Probably due to Dropbox server restarting, should retry
            mErrorMsg = "Dropbox error.  Try again.";
        } catch (DropboxException e) {
            // Unknown error
            mErrorMsg = "Unknown error.  Try again.";
        } catch (FileNotFoundException e) {
        }
        return false;
    }

    @Override
    protected void onProgressUpdate(Long... progress) {
        int percent = (int) (100.0 * (double) progress[0] / mFileLen + 0.5);
        mDialog.setProgress(percent);
    }

    @Override
    protected void onPostExecute(Boolean result) {
        mDialog.dismiss();
        if (result) {
            showToast("Successfully uploaded");
        } else {
            showToast(mErrorMsg);
        }
    }

    private void showToast(String msg) {
        Toast error = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
        error.show();
    }
    private void clearKeys() {
        SharedPreferences prefs = mContext.getSharedPreferences(ACCOUNT_PREFS_NAME, 0);
        Editor edit = prefs.edit();
        edit.clear();
        edit.commit();
    }



}

Now you should be able to use it to upload files! Here is an example:

File file = new File("/sdcard/some_file_I_have");
UploadFile upload = new UploadFile(this, mDBApi, "/", file);
upload.execute();

So you create new file of type File by passing in a string of path argument to the file you want to upload. The third argument in the UploadFile constructor indicates which folder of the Dropbox directory you would like to upload to. "/" indicates that it will happen in the root of your Dropbox Directory. If you would like a callback action when the upload is complete, you can implement that in the onPostExecute override in the UploadFile class. That's it! You should now have a working Dropbox uploader.

1 comment :

  1. I am a new to Android Development. I wish to select an image or a video from the Gallery of an Android Device and upload to Dropbox. Can you share your source code sir?

    ReplyDelete