Dropbox API v2をAndroidで使ってみる

はじめての投稿です。よろしくお願いします。

# 概要

[Dropbox v1 APIの廃止](https://blogs.dropbox.com/developers/2017/09/api-v1-shutdown-details)に伴い、AndroidアプリでDropbox v2 APIを使ったファイルのアップロード・ダウンロード機能を実装したのでメモを残します。
今回、v1 APIを使って以前実装したものをv2版に置き換えたのですが、本記事では新たにDropbox APIを使う方向けに導入手順から記載しています。

# 環境

- 開発環境
    - Android Studio 2.0
    - Gradle 2.10
    - JRE 1.7

- 動作環境
    - 端末: SHV34
    - OS: Android7.0

# 導入手順

導入でやることは下記のとおりです。

- DropboxのApp Consoleにアプリを登録する
- Dropbox v2 APIを取得する
- Manifestにpermissionを追加する
- ManifestにDropbox Activityを追加する


## DropboxのApp Consoleにアプリを登録する

（前提）Dropboxのアカウントが必要です。

１．Dropboxの[App Console](https://www.dropbox.com/developers/apps)を開き、[My apps]→[Create app]を選択します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/214985/00a2ddea-2ca9-7dbc-36c7-9d189f90032b.png" width=80%>

２．入力フォームの上から[Dropbox API]→[Full Dropbox]を選択し、アプリ名を入力して[Create app]を押します。
アプリ名はユニークな文字列でないといけないようです。画面の例では"test_app"と入力したところ重複エラーが出たため、特に意味もなく頭に"super_"を付けています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/214985/ea580e82-14bc-6ecf-b5c6-85ed3f1c2e8c.png" width=80%>

登録完了後、`App key`が実装で必要になります。後述の実装で`YOUR_DROPBOX_KEY`と記載している箇所はこの`App key`を指定してください。
【補足】v1 APIを使った実装では`App secret`も必要でしたが、v2 APIでは使用しなくても最低限動作することを確認しました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/214985/33317325-3a2e-a480-18ff-adade16c55a4.png" width=80%>


## Dropbox v2 APIを取得する

（以降、Android Studioで実施する手順になります。）

Dropbox v2 APIを取得するため、build.gradle(Module: app)の`dependencies`に下記を追加します。
ここではv3.0.5を取得していますが、[Dropbox v2 API](https://github.com/dropbox/dropbox-sdk-java)を適宜確認して最新のバージョンを取得してください。

```xml:build.gradle
dependencies {
    compile 'com.dropbox.core:dropbox-core-sdk:3.0.5'
}
```

## Manifestにpermissionを追加する

ネットワーク通信、及びファイルのアップロード・ダウンロードを行うため、下記のpermissionをAndroidManifest.xmlに追加します。

```xml:AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
```

## ManifestにDropbox Activityを追加する

AndroidManifest.xmlにDropbox Activityを追加します。
これはDropboxが提供している認証用Activityで、ここで先ほどの`App key`を指定して登録したアプリと紐づけます。

```xml:AndroidManifest.xml
       <activity
            android:name="com.dropbox.core.android.AuthActivity"
            android:configChanges="orientation|keyboard"
            android:launchMode="singleTask">
            <intent-filter>
                <data android:scheme="YOUR_DROPBOX_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>
```

【補足】これを記述せずに、後述の認証要求`Auth.startOAuth2Authentication(mContext, DROP_BOX_KEY);`を実行すると、下記のエラーが出ます。

`java.lang.IllegalStateException: URI scheme in your app's manifest is not set up correctly. You should have a com.dropbox.core.android.AuthActivity with the scheme: YOUR_KEY`

## ProGuardに追加する

proguard-rules.proに下記を追加します。
これを記述せずに署名付きapkを作成すると、エラーになります。

```bash:proguard-rules.pro
# OkHttp and Servlet optional dependencies
-dontwarn okio.**
-dontwarn okhttp3.**
-dontwarn com.squareup.okhttp.**
-dontwarn com.google.appengine.**
-dontwarn javax.servlet.**

# Support classes for compatibility with older API versions
-dontwarn android.support.**
-dontnote android.support.**
```


# 実装

## 処理の流れ

操作の流れに沿って、処理の流れを説明します。

（認証していない場合）

1. アップロードボタンを押す。
    - `onClick(View v)`が呼ばれ、`mDropboxManager.startAuthenticate()`を実行します。

2. 認証画面(*1)が開くので「許可」を押す。
    - アプリ画面に戻り、`onResume()`が呼ばれます。
    - ` mDropboxManager.getAccessToken();`で取得したトークンを使って、`new DropboxManager(this, token);`でインスタンス化します。ここでトークンをPreferenceに保存します。
    - 非同期タスク`AsyncHttpRequest`内でアップロード`mDropboxManager.backup(srcFilePath, dstFilePath);`を実行します。


（認証済みの場合）

1. アップロードボタンを押す。
    - `onClick(View v)`が呼ばれ、`mDropboxManager.startAuthenticate()`を実行します。
    - 保存したトークンを用いて`new DropboxManager(this, token);`でインスタンス化します。
    - 非同期タスク`AsyncHttpRequest`内でアップロード`mDropboxManager.backup(srcFilePath, dstFilePath);`を実行します。

ダウンロード処理においても、処理の流れは同じです。

(*1) 認証画面

<img src="https://qiita-image-store.s3.amazonaws.com/0/214985/d1ecff99-6cb3-01b7-a355-58b30719a488.png" width=60%>

## アップロード・ダウンロード処理について

Android端末内のファイル``"test.splite"``をDropboxに``"db_data.bak"``としてアップロードしています。
ダウンロードでは、逆にDropbox上の``"db_data.bak"``をダウンロードして、``"test.splite"``として保存しています。

## ソースコードの記載について

本題以外のソースコードを最小限にするため、文字列は直接記載しており、例外処理やダイアログ・トースト表示も極力省略しています。

## ソースコード

### Activity

```java:BackupActivity.java
public class BackupActivity extends Activity implements View.OnClickListener {

    private enum BackupAction {
        NONE,
        BACKUP_DROPBOX,
        RESTORE_DROPBOX,
    }

    private BackupAction mCurrentAction;
    private SharedPreferences mPref;
    private DropboxManager mDropboxManager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_backup);

        mPref = PreferenceManager.getDefaultSharedPreferences(this);
        mCurrentAction = BackupAction.NONE;

        ((Button) findViewById(R.id.backup_backup_dropbox)).setOnClickListener(this);
        ((Button) findViewById(R.id.backup_restore_dropbox)).setOnClickListener(this);
    }

    /**
     * Dropboxの認証画面から戻った際に呼ばれる。
     * バックアップ・復元を実行する。
     */
    @Override
    protected void onResume() {
        super.onResume();
        switch (mCurrentAction) {
            case NONE:
                break;
            case BACKUP_DROPBOX:
                mCurrentAction = BackupAction.NONE;

                try {
                    String token = mDropboxManager.getAccessToken();
                    if(token == null) {
                        // 認証失敗
                        break;
                    }
                    mDropboxManager = new DropboxManager(this, token);

                    // 次回以降に認証を省くため、トークンを保存する
                    SharedPreferences.Editor edit = mPref.edit();
                    edit.putString("access_token", token);
                    edit.commit();

                    // アップロードを実行
                    new AsyncHttpRequest(this, BackupAction.BACKUP_DROPBOX).execute();

                } catch (IllegalStateException e) {
                    // IllegalStateException
                }
                break;
            case RESTORE_DROPBOX:
                mCurrentAction = BackupAction.NONE;

                try {
                    String token = mDropboxManager.getAccessToken();
                    if(token == null) {
                        // 認証失敗
                        break;
                    }

                    mDropboxManager = new DropboxManager(this, token);

                    // 次回以降に認証を省くため、トークンを保存する
                    SharedPreferences.Editor edit = mPref.edit();
                    edit.putString("access_token", token);
                    edit.commit();

                    // ダウンロードを実行
                    new AsyncHttpRequest(this, BackupAction.RESTORE_DROPBOX).execute();
                } catch (IllegalStateException e) {
                    // IllegalStateException
                }
                break;
            default:
                break;
        }
    }

    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.backup_backup_dropbox:
                // 認証済みの場合はアップロードを実行、未認証の場合は認証処理を行う
                if (mPref.contains("access_token")) {

                    if(mDropboxManager == null) {
                        String token = mPref.getString("access_token", null);
                        mDropboxManager = new DropboxManager(this, token);
                    }

                    // アップロードを実行
                    new AsyncHttpRequest(this, BackupAction.BACKUP_DROPBOX).execute();

                } else {
                    mCurrentAction = BackupAction.BACKUP_DROPBOX;

                    if(mDropboxManager == null) {
                        mDropboxManager = new DropboxManager(this);
                    }
                    // 認証ページを開く
                    mDropboxManager.startAuthenticate();
                }
                break;

            case R.id.backup_restore_dropbox:
                // 認証済みの場合はダウンロードを実行、未認証の場合は認証処理を行う
                if (mPref.contains("access_token")) {

                    if(mDropboxManager == null) {
                        String token = mPref.getString("access_token", null);
                        mDropboxManager = new DropboxManager(this, token);
                    }

                    // ダウンロードを実行
                    new AsyncHttpRequest(this, BackupAction.RESTORE_DROPBOX).execute();
                } else {
                    mCurrentAction = BackupAction.RESTORE_DROPBOX;

                    if(mDropboxManager == null) {
                        mDropboxManager = new DropboxManager(this);
                    }
                    // 認証ページを開く
                    mDropboxManager.startAuthenticate();
                }
                break;
            default:
                break;
        }
    }

    /**
     * 非同期処理終了後に呼び出される。
     * メッセージを表示する。
     */
    public void callback(BackupAction action, boolean result) {
        switch (action) {
            case BACKUP_DROPBOX:
                if (result) {
                    // success
                } else {
                    // failed
                }
                break;
            case RESTORE_DROPBOX:
                if (result) {
                    // success
                } else {
                    // failed
                }
                break;
            default:

                break;
        }
    }

    /**
     * アップロード・ダウンロードのHTTPリクエストを行う非同期処理タスク。
     */
    public class AsyncHttpRequest extends AsyncTask<Void, Void, Boolean> {

        private BackupActivity activity;
        private BackupAction action;
        private ProgressDialog progressDialog;

        public AsyncHttpRequest(BackupActivity activity, BackupAction action) {
            this.activity = activity;
            this.action = action;
        }

        @Override
        protected void onPreExecute() {
            // プログレスダイアログの生成
            this.progressDialog = new ProgressDialog(activity);
            switch (action) {
                case BACKUP_DROPBOX:
                    this.progressDialog.setTitle("アップロード中...");
                    this.progressDialog.setMessage("アップロード中です。このまましばらくお待ちください");
                    break;
                case RESTORE_DROPBOX:
                    this.progressDialog.setTitle("ダウンロード中...");
                    this.progressDialog.setMessage("ダウンロード中です。このまましばらくお待ちください");
                    break;
                default:
                    break;
            }
            this.progressDialog.setCancelable(false); // キャンセル不可にする
            this.progressDialog.show();
            return;
        }

        @Override
        protected Boolean doInBackground(Void... builder) {
            boolean isSuccess = false;
            String srcFilePath = "/data/data/" + getPackageName() + "/databases/test.splite"; // Android端末内のSQLiteのデータ
            String dstFilePath = "db_data.bak"; // Dropboxに保存される際のファイル名

            switch (action) {
                case BACKUP_DROPBOX: {
                    isSuccess = mDropboxManager.backup(srcFilePath, dstFilePath);
                    break;
                }
                case RESTORE_DROPBOX: {
                    isSuccess = mDropboxManager.restore(dstFilePath, srcFilePath);
                    break;
                }
                default:
                    break;
            }
            return isSuccess;
        }

        @Override
        protected void onPostExecute(Boolean result) {
            // プログレスダイアログを閉じる
            if (this.progressDialog != null && this.progressDialog.isShowing()) {
                this.progressDialog.dismiss();
            }             
            activity.callback(action, result);
        }
    }
}
```

### DropboxManager

```java:DropboxManager.java
public class DropboxManager {

    private Context mContext;
    private DbxClientV2 mClient;

    private final static String DROP_BOX_KEY = "YOUR_DROPBOX_KEY";

    /**
     * コンストラクタ。
     * @param context コンテキスト
     */
    public DropboxManager(Context context) {
        mContext = context;
    }

    /**
     * コンストラクタ。認証済みの場合はこちらを使用する。
     * @param context コンテキスト
     * @param token 認証済みトークン
     */
    public DropboxManager(Context context, String token) {
        mContext = context;
        DbxRequestConfig config = new DbxRequestConfig("Name/Version");
        mClient = new DbxClientV2(config, token);
    }

    /**
     * 認証処理を開始する。
     * 認証ページが開く。
     */
    public void startAuthenticate() {
        Auth.startOAuth2Authentication(mContext, DROP_BOX_KEY);
    }

    /**
     * 認証トークンを取得する。
     * @return 認証トークン。
     */
    public String getAccessToken() {
        return Auth.getOAuth2Token();
    }

    /**
     * ファイルをバックアップする。
     * @param srcFilePath 保存元のファイルパス(Android)
     * @param dstFilePath 保存先のファイルパス(Dropbox)
     * @return 成功した場合はtrue,失敗した場合はfalseを返す
     */
    public boolean backup(String srcFilePath, String dstFilePath) {
        try {
            File file = new File(srcFilePath);
            InputStream input = new FileInputStream(file);
            byte[] bytes = convertFileToBytes(file);
            // Dropboxのファイルパスは先頭に"/"と指定する必要があるため、ファイルパスの先頭に"/"をつける
            mClient.files().uploadBuilder("/" + dstFilePath)
                    .withMode(WriteMode.OVERWRITE)
                    .uploadAndFinish(input);

        } catch (Exception e) {
            Log.e("tag", "Upload Error: " + e);
            return false;
        }
        return true;
    }

    /**
     * ファイルを復元する。
     * @param srcFilePath 復元元のファイルパス(Dropbox)
     * @param dstFilePath 復元先のファイルパス(Android)
     * @return 成功した場合はtrue,失敗した場合はfalseを返す
     */
    public boolean restore(String srcFilePath, String dstFilePath) {
        try {
            // ファイルを検索する
            SearchResult result = mClient.files().search("", srcFilePath);
            List<SearchMatch> matches = result.getMatches();
            LogUtil.d("matches.size(): " + matches.size());

            Metadata metadata = null;
            for (SearchMatch match : matches) {
                metadata = match.getMetadata();
                break;
            }

            if(metadata == null) {
                // ファイルが見つからない場合
                Log.d("tag", "metadata not found");
                return false;
            } else {
                Log.d("tag", "metadata.getPathLower(): " + metadata.getPathLower());
            }

            // ダウンロードし、ファイルを置き換える
            File file = new File(dstFilePath);
            OutputStream os = new FileOutputStream(file);
            mClient.files().download(metadata.getPathLower()).download(os);

        } catch (Exception e) {
            Log.e("tag", "Download Error: " +  e);
            return false;
        }
        return true;
    }

    /**
     * Fileをbytes[]に変換する。
     * @param file ファイル
     * @return bytes
     * @throws IOException
     */
    private byte[] convertFileToBytes(File file) throws IOException {
        final long fileSize = file.length();
        final int byteSize = (int) fileSize;
        byte[] bytes = new byte[byteSize];
        try {
            RandomAccessFile raf = new RandomAccessFile(file, "r");
            try {
                raf.readFully(bytes);
            } finally {
                raf.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bytes;
    }
}
```

### activity_backup.xml

```xml:activity_backup.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/backup_backup_dropbox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Dropboxにアップロードする" />

    <Button
        android:id="@+id/backup_restore_dropbox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Dropboxからダウンロードする" />
</LinearLayout>
```

# 参考

[Dropbox v2 API](https://github.com/dropbox/dropbox-sdk-java)
examples/androidにAndroidアプリのサンプルコードもあります。