Xamarin Android Dropbox API v2

In this post we look at how to implement the Dropbox API V2 in a Xamarin Android project via. the Dropbox.API NuGet package. We look at file I/O and authentication as part of this.

Advertisements

DropBox API version 1 is dead, and anyone using the Xamarin component stores implementation of the Dropbox API is probably already aware of this. I was absolutely delighted to come into work one day, only to find that all the code I had written for Dropbox support was no longer working. Woohoo. So yeah, Dropbox hasn’t kept this a secret or anything, no one who was paying attention should have been surprised, but then again…some of us weren’t paying attention. In this post, I’m going to share with you the code I wish I could have found online, but literally no one had shared for Xamarin.Android, it was always “this works for WPF but not Android”, or some other platform or project type.

I mentioned the component store being dead, more info here. Basically Xamarin is sunsetting the component store to get the .NET ecosystem all on the same page with using NuGet as the primary package manager. Fortunately for me, I was already using the Dropbox API from the NuGet package for some of my Dropbox functionality, so all I really had to do was re-write authentication code.

Ditch that component, get the NuGet package

If you have the component for Dropbox, ditch it, it hasn’t been updated since a year or two ago (I don’t remember exactly), either way it’s dead, and Xamarin has proven they won’t be keeping components up to date, so don’t bet on them doing it. Instead, go download the Dropbox NuGet package which is maintained by Dropbox called “Dropbox.Api”. This package has built in methods for accessing the API v2 endpoints.

Login View

First and foremost, you need to make an app under your Dropbox account, and get the app key and app secret. Once you have this, put them somewhere safe in your project, I throw them under a shared static class as readonly strings. The first thing you will need to do is get a DropBox access token, and to do this will require you to create an activity and a view, here’s the view I created.

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

Dropbox authentication activity

Okay, that’s the view, so what’s the activity look like?

[Activity(Label = "DropboxAccountAuthenticationActivity")]
public class DropboxAccountAuthenticationActivity : Activity
{
 protected override void OnCreate(Bundle savedInstanceState)
 {
  base.OnCreate(savedInstanceState);
  SetContentView(Resource.Layout.DropboxAccountAuthentication);
  SetupDropboxFunctionality();
 }

 private void SetupDropboxFunctionality()
 {
  var dropboxUtilities = new Utility.DropboxUtilities();

 &nbsp// Load uri in webview
  var webview = FindViewById(Resource.Id.DropboxAuthenticationWebview);
  webview.SetWebViewClient(new Utility.DropboxWebViewClient(this));
  webview.Settings.JavaScriptEnabled = true;

  var uri = dropboxUtilities.GetAuthorizationUri();
  webview.LoadUrl(uri.AbsoluteUri);
 }

 public override void Finish()
 {
  base.Finish();
  StartActivity(new Intent(this, typeof(MainActivity)).AddFlags(ActivityFlags.NoHistory));
 }
}

Alright, so that’s that, but you’ll notice there’s probably some errors because it can’t find Dropbox utilities. Before I give you the code for this class, unless you plan on changing the way this code works, you need to have a static class to store the following static members values along with your Dropbox app key and Dropbox app secret:

Dropbox utilities

This is a custom class I wrote for handling all the various Dropbox functionality, I’m just going to post the entire class and when we get to file I/O you can just have those methods too. (For what it’s worth I tried formatting it with whitespace but WordPress make that a living hell to do so…sorry)


public class DropboxUtilities
{
public Uri GetAuthorizationUri()
{
Shared.DropboxOAuth2State = Guid.NewGuid().ToString("N");
try
{

var authorizationUri = DropboxOAuth2Helper.GetAuthorizeUri(oauthResponseType: OAuthResponseType.Token,
clientId: Shared.DropboxAppKey,
redirectUri: new Uri(Shared.DropboxRedirectUri),
state: Shared.DropboxOAuth2State, forceReapprove: false,
disableSignup: true);

return authorizationUri;
}
catch(Exception e)
{
return null;
}
}

///

/// Call this in OnResume to finalize the OAuth2 authorization, returns from Dropbox app or browser
/// 

/// Calling activity
/// Instance of Dropbox api, keep this local to the activity
/// Optionally store the access token as a shared preference
/// Authorization token, or empty string is authorization was unsuccessful
public void FinishOAuth2Authorization(Activity context, bool storeAccessTokenAsSharedPreference)
{
try
{
// Store access token as a shared preference,
// false for if we want the user to authorize the next time the app loads.
if (storeAccessTokenAsSharedPreference)
{
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(context);
ISharedPreferencesEditor editor = prefs.Edit();
editor.PutString("DropboxAccessToken", Shared.DropboxAccessToken);
editor.Apply();
}

}
catch (Java.Lang.IllegalStateException ex)
{
Toast.MakeText(context, "Failed to authorize Dropbox.", ToastLength.Long).Show();
ex.Dispose();
}
}

/// Access token if present, empty string if not found
public string GetDropboxAccessTokenFromSharedPreferences(Activity context, string preferenceKey)
{
var preferences = PreferenceManager.GetDefaultSharedPreferences(context);
return preferences.GetString(preferenceKey, "");
}

///

/// When searching for the app folder, pass an empty string
/// (app-folder-only apps get root access at their own folder by default)
/// 

///
public bool DropboxFolderExists(Activity context, string path, Dropbox.Api.DropboxClient client)
{
try
{
if (Shared.DropboxAccessToken == "")
{
var t = Toast.MakeText(context, "Dropbox Access Denied!", ToastLength.Short);
t.Show();
t.Dispose();
}

var folders = client.Files.ListFolderAsync(path);
var result = folders.Result;
return true;
}
catch (Exception ex)
{
return false;
}
}

public Dropbox.Api.Files.ListFolderResult GetFileListFromFolder(string path, Dropbox.Api.DropboxClient client)
{
var files = client.Files.ListFolderAsync(path);
return files.Result;
}

///

/// When searching for the app folder, pass an empty string
/// (app-folder-only apps get root access at their own folder by default)
/// 

public bool CreateDropboxAppFolder(Activity context, string path, Dropbox.Api.DropboxClient client)
{
try
{
if (Shared.DropboxAccessToken == "")
{
var t = Toast.MakeText(context, "Dropbox Access Denied!", ToastLength.Short);
t.Show();
t.Dispose();
}

var folderArg = new Dropbox.Api.Files.CreateFolderArg(path);
var folder = client.Files.CreateFolderV2Async(folderArg);
var result = folder.Result;
return true;
}
catch (Exception e)
{
return false;
}
}

///

/// If calling multiple times for file uploads, pass a semaphore with an initial/max count of 2 or 3 max due to rate limits.
/// 

/// Dropbox client
/// The dropbox folder to use
/// The name of the file
/// The actual file object
/// Optional semaphore slim for multi-file uploads
/// File metadata for uploaded file
public async Task UploadFileToDropboxAsync(Dropbox.Api.DropboxClient client,
string folder, string file,
Java.IO.File javaFile, SemaphoreSlim locker = null)
{
FileInputStream fileInputStream = new FileInputStream(javaFile);

var byteLength = javaFile.Length();
byte[] fileBytes = new byte[byteLength];
fileInputStream.Read(fileBytes, 0, (int)byteLength);
var memoryStream = new System.IO.MemoryStream(fileBytes);

// NOTE: Cannot exceed a file size of 150mb.
try
{
// Wait for tasks to finish
if(locker != null)
{
await locker.WaitAsync();
}

var uploaded = await client.Files.UploadAsync(
folder + "/" + file,
Dropbox.Api.Files.WriteMode.Overwrite.Instance,
body: memoryStream);

return uploaded;

}
catch(Dropbox.Api.RateLimitException e)
{
// Section still locked, run normally.

// RetryAfter is the amount of seconds Dropbox recommends you wait before trying the request again
try
{
Thread.Sleep(e.RetryAfter * 1000);

var uploaded = await client.Files.UploadAsync(
folder + "/" + file,
Dropbox.Api.Files.WriteMode.Overwrite.Instance,
body: memoryStream);
return uploaded;
}
catch(Dropbox.Api.RateLimitException ex)
{
return null;
}
}
finally
{
// Once all code has executed, unlock.
if (locker != null)
{
locker.Release();
}

fileInputStream.Dispose();
memoryStream.Dispose();
}
}
}

So in order, we’ve got a method to get the authorization uri, which is what we will load into our webview to redirect to Dropbox’s authenticator. This requires a redirect uri, as you can see I’ve passed it “Shared.DropboxRedirectUri”, this is what that string looks like: “https://www.dropbox.com/1/oauth2/redirect_receiver&#8221;. To be honest with you, I have absolutely no idea why that uri works, I had to do some serious digging to find that, but it does work.

We also have a method that finishes out the OAuth authorization, this is the method that gets called once Dropbox authorization finishes and it’s how we get the dropbox access token from Dropbox. Note that this method also will store the access token as a shared preference, this means it caches the access token for future reference so the user doesn’t have to re-authenticate as much. I’ll show you how to implement this method in a moment.

Below that, we have a method for getting the access token from shared preferences, and then we have a few Dropbox file I/O helpers. One for checking if a folder exists, another for getting a list of files under a certain path, one for creating the app folder (if app is requiring app level folder access, this path is simply an empty string), and finally one for doing file uploads.

I want to briefly discuss the final method present in this file, UploadFileToDropboxAsync. This method is pretty standard stuff, except the SemaphoreSlim. Now my guess is most developers probably haven’t even heard of a semaphore, let alone know what a semaphore slim is or what it does, and even fewer have probably actually used one. To put it very, very simply (I will do a future post on this topic I promise), a semaphore is a locking mechanism for doing batched asynchronous work. So there’s two locks, a Mutex and a Semaphore, mutex being short for Mutually exclusive, meaning 1 lock, 1 key, and semaphore being a locking mechanism which allows multiple threads to work on a single piece of data of some kind. In this case, I used a semaphore to limit the number of concurrent uploads that can occur at once, so in this case I believe I set the limit to be 3 files at once, maybe 2, but this has to occur because if it doesn’t, you have to do the uploads synchronously which is super slow, or some of your files won’t upload due to rate limit exceptions…oh yeah, and the slim part of semaphore slim just means it’s a trimmed down version of a normal semaphore. This method will also attempt to do one re-upload if the rate-limit is met, it bases this off the exceptions RetryAfter property, something Dropbox sends back as a recommended time to wait before trying again.

Implementing it all in the calling activity

Whatever activity you need to use this stuff in, this will apply all the same, you just will use your activity of choice rather than main. I’m not going to post my whole activity as frankly there’s a lot of proprietary code there, but I’ll share the relevant bits. First off, the local members you’ll need to declare at the class level:


static SemaphoreSlim batchedUploadLock = new SemaphoreSlim(2, 2);

In OnCreate, implement this code at some point to do initialization:


// Create and authorize dropbox session
Utility.DropboxUtilities dropboxUtilities = new Utility.DropboxUtilities();
// If booting up the app, the access token will be empty, try reading from shared preferences
if (Shared.DropboxAccessToken == "")
{
Shared.DropboxAccessToken = dropboxUtilities.GetDropboxAccessTokenFromSharedPreferences(this, Shared.DropboxPreferencesKey);
}
// Otherwise, the token exists from an authorization but we haven't stored it in shared preferences, do this once.
else if(!Shared.DropboxWasAuthorized)
{
dropboxUtilities.FinishOAuth2Authorization(this, true);
Shared.DropboxWasAuthorized = true;
}

This will do all your setup for you when the activity loads. Next, add your handler for actually creating the authentication activity by adding the following code:


// Setup dropbox
if (Shared.DropboxAccessToken == "")
{
StartActivity(new Intent(this, typeof(DropboxAccountAuthenticationActivity)).AddFlags(ActivityFlags.NoHistory));
}

Now I’ll share with you a super stripped down version of what I did on this clients app to handle file uploads, but there’s a lot of UI related stuff I did that doesn’t apply generically, so I’m going to leave those bits out.

Folder selection for upload

First, I make sure there are files to be uploaded, this list is pre-filled in an earlier section based on selections in a recycler view:


if (filesToUpload.Count > 0)

In this if, I get a list of folders and prompt the user for which folder they would like to use to upload to, and I create an alert which simply holds the folder names. To get the folders and handle multiple uploads I use this:

var folderResult = dropboxUtilities.GetFileListFromFolder("", client);
var folders = folderResult.Entries.Where(entry => entry.IsFolder).ToList();

/////////CREATE AN ALERT///////////////

alert.SetAdapter(arrayList, (alertSender, ev) => {
var selectedFolderPath = "";
var selectedFolder = arrayList.GetItem(ev.Which);
// If selected folder is not the root, get the selected folder path
if (selectedFolder != "/")
{
selectedFolderPath = folderResult.Entries.Where(entry => entry.Name == selectedFolder).Select(entry => entry.PathLower).FirstOrDefault();
}

List uploadTasks = new List();
foreach (var file in filesToUpload)
{
uploadTasks.Add(Task.Factory.StartNew(() =>
{
// Have to provide a locking mechanism due to rate limits on multiple writes.
var t = dropboxUtilities.UploadFileToDropboxAsync(client, selectedFolderPath, file.Name, file, batchedUploadLock);
return t.Result;
}));
}

Task.WaitAll(uploadTasks.ToArray());

This will allow the user to select a folder to upload files to, and upload multiple files. If you only care about one file upload, then just do a single upload rather than multiple, pretty simple stuff. To check which files weren’t uploaded successfully, you can just take your list of upload tasks, check where the result wasn’t null, and find which files aren’t in that list compared against the original file list.

Conclusion

There’s a lot more to the DropBox API than what I mentioned here, but this will give you a foundation to build off of, and I should mention, this code is almost identical if you’re doing a Xamarin.iOS app, the only real differences are in how files are read in, and how the authentication works in terms of the Dropbox authenticator. I will most likely post a follow up to this with an iOS implementation if there is any interest, leave a comment below or email me if you would like to see an iOS implementation of this, and feel free to let me know if you liked this post or hated it. Until next time.

 

  1. […] on April 1, 2018by admin submitted by /u/Trevor266 [link] [comments] No comments […]

    Like

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

Advertisements
Advertisements
%d bloggers like this: