東京工業大学
デジタル創作同好会

2017年4月9日 | メンバーブログ

AndroidアプリでGoogle Driveと連携する 新歓ブログリレー2017 7日目

yuu

この記事は、新歓ブログリレー2017の7日目のものです。
 
 新入生の皆々様、ご入学おめでとうございます。
 こちらの記事は、情報工学科3年のyuuがお送りいたします。
 これから始まる大学生活に不安のある方もいらっしゃるとは思います。しかし、日本の大学は入るは難く出るは易しとも言いますし、まあ意外と何とかなるので、健康と落単には気を付けて有意義な学生生活をお過ごしください。
 
 堅苦しい挨拶はこの辺にして、閑話休題、本題に入ろうかと思います。

はじめに

さて、Google Driveってなんぞという方はあまりいらっしゃらないとは思いますが、手短に説明しておきます。
 Google Driveとは、Googleが提供する無料のクラウドストレージサービスです。簡単に言えば、タダでファイルを保存したり、共有できるサービスというわけです。
 
 ここで、自分で作ったアプリのデータや画像なんかを、Google driveに投げておいて、Android同士やパソコンなんかで同期できたら素敵だと思いませんか? 今回はそういう話になります。
 技術的な話題になりますので、分からない方はそういう機能があることを心に留めておいて、必要な時に思い出していただけると幸いです。

なお、環境はAndroid Studio、開発言語はKotlinになります。Kotlinを知らないという方は、ほとんどJavaだと思ってもらっても、あまり問題はありません。

この記事で取り扱うこと

この記事では、基本的なAndroidアプリケーションの作り方は紹介しません。あくまで、Google DriveのAPIを利用する方法のみに眼目を置きます。そのため、Androidアプリケーションの作り方などはググって下さい。今日日、ググれば始めるまでの流れは大体わかります。
 以上の理由により、このブログでは以下を紹介します。

  1. Google DriveのAPIを使うための前準備
  2. Google DriveのAPIを使った様々な操作

Google Drive APIの種類

Google DriveをAndroidから使うには、二つの方法があります。
 一つは、Google Drive Android APIです。これは、そのアプリ自身が作ったファイルや、ユーザーに一々選ばせたファイルにしかアクセスできません。その自由度の低さの代わりに、色々と取り扱いが便利です。
 もう一つは、Google APIs Client Library for Javaです。これは、Google Driveへのフルアクセスが可能です。その代わりに、色々と面倒な部分があります。
 今回は、自由度の高いGoogle APIs Client Library for Javaの使い方を説明します。

前準備

ライブラリを利用するために、appフォルダー内のbuild.gradleに以下の追加が必要になります。


dependencies {
    // 既にある要素
    compile "com.google.android.gms:play-services-drive:10.2.1"
    compile 'com.google.android.gms:play-services-auth:10.2.1'
    compile 'com.google.apis:google-api-services-drive:v2-rev96-1.16.0-rc'
    compile 'com.google.api-client:google-api-client:1.20.0'
    compile 'com.google.api-client:google-api-client-android:1.20.0'
    compile 'com.google.http-client:google-http-client-gson:1.16.0-rc'
}

また、認証のため、AndroidManifest.xmlに以下の記述を追加します。(Android6.0以降は、実行時認証らしいのでもしかしたら不要かもしれません)

    // packageうんたらかんたら>

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

    //<applicationうんたらかんたら

実践

はじめに基本的な使い方を理解しておく

まずは、このAPIの基本的な使い方を理解しておきましょう。
 Google Drive APIでは、基本的に以下の流れで処理を行います。

  1. クエリを飛ばして検索して、ファイルのメタデータを取得する
  2. メタデータからIDやそのほかの情報を取得する
  3. IDを用いてファイルの詳細を取得したり、メタデータを利用した操作をしたりする

このように、Google Driveではまずメタデータを取得します。そのメタデータから取得したユニークなIDでファイルを識別し、IDを介して種々の操作を行います。
 また、今回はあまり触れませんが、メタデータからはID以外にも様々な情報が取得できますので、適宜使うことができます。
 それでは、基本的な使い方が分かったところで、認証に移りましょう。

認証する

Google Driveにアクセスするためには、様々な権限の認証が必要です。
 まずは、Googleのアカウントを取得するための権限の認証を行います。


    val permissions = ArrayList()
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.GET_ACCOUNTS) != PackageManager.PERMISSION_GRANTED)
        if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.GET_ACCOUNTS))
            permissions.add(Manifest.permission.GET_ACCOUNTS)
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED)
        if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.INTERNET))
            permissions.add(Manifest.permission.INTERNET)
    if(permissions.isNotEmpty())
        ActivityCompat.requestPermissions(this, permissions.toTypedArray(), MY_PERMISSIONS_REQUEST_READ_CONTACTS)

次にアカウントを認証する必要があります。以下のコードで認証が可能です。


    // この定数は重複しないユニークな値なら何でもいいです
    private val REQUEST_ACCOUNT = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        credential = GoogleAccountCredential.usingOAuth2(this, Arrays.asList("https://www.googleapis.com/auth/drive"))
        startActivityForResult(credential?.newChooseAccountIntent(), REQUEST_ACCOUNT)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when(requestCode) {
        // ユーザー設定後に遷移
            REQUEST_ACCOUNT -> {
                if(resultCode == Activity.RESULT_OK && data != null && data.extras != null) {
                    // 名前情報を取得し、nullでなければ設定
                    val name = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
                    if(name != null) {
                        credential?.selectedAccountName = name
                        service = com.google.api.services.drive.Drive.Builder(AndroidHttp.newCompatibleTransport(), GsonFactory(), credential).build()
                    }
                }
            }
        }
    }

以上で使うための認証は終わりました。これで、様々な操作が行えるようになります。早速、見ていきましょう。

検索する

まずはファイルのIDを取得する必要があります。これは、Google Driveにクエリを飛ばして検索することで実現できます。


    (object : AsyncTask() {
        override fun doInBackground(vararg params: Void?){
            var token : String? = null
            try {
                do {
                    val result = service?.files()?.list()
                            ?.setQ("title = 'DrawingSupporter.jar'")
                            ?.setPageToken(token)
                            ?.execute()
                    // ゴミ箱にあるファイルを取り除いて逐次実行
                    result?.items?.filterNot { it.labels.trashed }?.forEach {
                        // IDは、it.id
                        // 親フォルダは、it.parents
                    }
                    token = result?.nextPageToken
                } while(token != null)
            } catch (e: UserRecoverableAuthIOException) {
                // 認証がない場合の処理
            } catch (e: IOException) {
                // 読み込めないときの処理
            } catch (e: SocketTimeoutException) {
                // タイムアウト時の処理
            }
        }
    }).execute()

上記コードの注意点はいくつかあります。
 まず、Google DriveのAPIを用いる時は、別スレッドで行います。Androidの仕様上、メインスレッドではHTTP通信できないためです。以降のすべての操作も同様のため、今後のコードではAsyncTaskでくるむ部分は端折ります。
 次に、色々と例外処理が出るということです。これらの処理は設計者の手に委ねられますので、今後のコードではこのtry-catch説も端折ります。
 最後に、一々、nextPageTokenを用いてtokenを取得しています。これは、検索結果が膨大な量になった場合、分割して取得するための方法です。今後のコードでは、このdo-while文も端折ります。
 
 では、注意点を鑑みて、以下のコードを説明したいと思います。


    val result = service?.files()?.list()
            ?.setQ("title = 'targetName'")
            ?.setPageToken(token)
            ?.execute()
    // ゴミ箱にあるファイルを取り除いて逐次実行
    result?.items?.filterNot { it.labels.trashed }?.forEach {
        // IDは、it.id
        // 親フォルダは、it.parents
    }

まず、検索内容を決めるためにsetQメソッドを用います。この中には、検索するための命令を仕込むことになります。その文法については、このページに載っています。しかし、何故かファイル名で指定して検索する方法が「name = 'name'」になっていますが、当方が試したところ「titie = "name"」でないとエラーになるためご注意ください。
 setQには、"'$id' in parents and (mimeType contains 'image/' or mimeType = 'application/vnd.google-apps.folder')"のように、andやorを用いて複雑な命令も出すことができます。有意義に使っていきましょう。なお、この命令はidで指定されたIDのフォルダを親に持つ、画像ファイルまたはフォルダを検索できます。
 
 こうすることで、前述したようにresultにファイルのメタデータが入りました。また、IDが事前に分かっている場合は、クエリを投げずに、直接取得することができます。次は直接取得する方法を見ていきましょう。

直接取得する


    val result = MainActivity.service?.files()?.get(dataID)?.execute()

直接取得するコードはすごいシンプルですね。dataIDに格納されたユニークなIDを持つファイルのメタデータを取得することができます。
 こうして取得したメタデータからはIDや、親フォルダの情報など、様々な情報が得られます。では、このIDを用いて、実際にダウンロード、アップロードなどの操作を見ていくことにしましょう。

ダウンロードする


    val out = ByteArrayOutputStream()
    MainActivity.service?.files()?.get(dataID)?.setAlt("media")?.executeAndDownloadTo(out)
    openFileOutput("text.db", Context.MODE_PRIVATE).write(out.toByteArray())

これは、dataIDのIDを持つファイルの内容を、ByteArrayOutputStreamを介して、text.dbというファイルに保存するコードです。
 これでダウンロードができます。見た目はシンプルなのですが、ダウンロード方法をググるとexecuteMediaAndDownloadToメソッドを使えというのに、このメソッドが見当たらないということに見舞われました。ドキュメントを読む限り、altパラメータにmediaを渡せば解決するらしいので、以上の方法になりました。
 
 なお、画像を取得して、即座に表示するだけで、保存はしたくないという場合も当然あるかと思います。その場合は、以下のようなコードになります。


    val out = ByteArrayOutputStream()
    override fun doInBackground(vararg params: Unit?) {
        try {
                MainActivity.service?.files()?.get(id)?.setAlt("media")?.executeAndDownloadTo(out)
        } catch (e: UserRecoverableAuthIOException) {
        } catch (e: IOException) {
        } catch (e : SocketTimeoutException) {
        }
     }

     override fun onPostExecute(param: Unit?) {
        val bytes = out.toByteArray()
        val image = findViewById(R.id.image) as ImageView
        image.setImageBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.size))
     }

先ほど断ったのにまたdoInBackgroundを書いているじゃないかと突っ込まれそうですが、これは結構重要なことなので省かずに書きました。
 AndoridのUIを変更させるのは、メインスレッドでなくてはなりません。そのため、別スレッドで画像を表示させる処理は行えません。
 しかし、onPostExecuteはdoInBackgroundが終わった後、メインスレッドで呼ばれますので、UIを変更させることができるというわけです。
 
 ともあれ、この方法で、現在のUIのid名がimageのImageViewに、ダウンロードした画像を表示させることができます。

アップロードする


    val uploadFile = File(imagePath)
    val file = com.google.api.services.drive.model.File().setTitle("test").setMimeType("image/jpeg")
            .setParents(Arrays.asList(parentID))
    val media = FileContent("image/jpeg", uploadFile)
    val result = MainActivity.service?.files()?.insert(file, media)?.execute()

このコードで、imagePathにあるファイルを、testという名前のJPEGファイルとして、parentIDを持つフォルダの直下にアップロードできます。
 もし、名前をtest2にしたいのであれば、setTitle("test2")とすれば良いです。
 また、JPEGファイル以外を使いたい場合は、MimeTypeを変えてください。ちなみに、'text/plain'でtxtファイルが扱えます。
 
 しかし、アップロードもいいですが、ファイル同期のためには、内容を書き換える必要も出てくるかもしれません。そういった場合の操作を見ていきましょう。

内容を書き換える


    val syncFile = File(filePath)
    val file = MainActivity.service?.files()?.get(dataID)?.execute()
    val media = FileContent(file?.mimeType, syncFile)
    MainActivity.service?.files()?.update(dataID, file, media)?.execute()

このコードで、dataIDを持つGoogle Driveのファイルの内容を、syncFileの内容に書き換えることができます。
 また、file.setTitle("rename")などを使えば、リネームすることもできるかもしれません(試してはいないですが)。

終わりに

というわけで、前準備、認証、検索、取得、ダウンロード、アップロード、書き換えといった処理を一通り説明してきました。
 今回この記事に先立ちGoogle Drive APIについて調べているときは大変苦労しました。ネットの海に広がっている情報はバージョンがどのあたりなのかもよくわからず、色々な情報が混合して、今は存在しないメソッドの話をしていたり、パラメータが間違っていたり……。
 何はともあれ、この記事がGoogle DriveのAPIを叩いてみようと思った新入生の助けになれば幸いです。

どうでもいい後書き

今回の記事ですが、最初は「幼年期の終わり」とか「アンドロイドは電気羊の夢を見るか?」、「蜘蛛女のキス」から「半七捕物帖」。いっそ「氷川清話」や「暗黒日記」。あるいは「戦争論」とか「君主論」。近いものなら「愚者のエンドロール」から「神様のメモ帳」あたりまで、好きな本の構成法とか書評を書くつもりでした。
 しかし、東工大生まして新入生には興味ないであろうことを斟酌して、今回の表題と相成った次第です。
 もし上記の本に興味があるという奇特な新入生の方はデジタル創作同好会traPへどうぞ。こういうのが好きな筆者でも住みよいサークルです。

ご案内

次の新歓ブログリレー2017 8日目の担当は、clkとkazがお送りします。音楽のプロとサークル代表という豪華な取り合わせなのでお見逃しの無いように。

この記事を書いた人
yuu

主にプログラマーをしながら、まれに趣味で小説を書いています。ゲームはするのも作るのも大好物です。

この記事をシェア

このエントリーをはてなブックマークに追加

関連する記事

2017年4月28日
モンハンにおけるダメージ計算【新歓ブログリレー2017 25日目】
Humming
2017年4月27日
合同変換及び運動について【新歓ブログリレー2017 24日目】
hatasa-y
2017年4月26日
第5回ゲーム制作者交流イベントGAME<sup>3</sup>を開催しました【新歓ブログリレー2017 23日目】
kaz

活動の紹介

カテゴリ

タグ