無料のボイスチェンジャーを色々と試した

概要

無料のボイスチェンジャーをいくつか試した結果、VSTHost + RoVee に落ち着きました。OBS Studio と合わせて使うことを前提にしています。

目次

参考情報

解説

まずは参考情報のサイトを参照しつつ、試してみるボイスチェンジャーのソフトウェアをピックアップしました。

今回試したソフトウェアは以下の

試行

恋声

恋声は以前にも別記事で使用しています。2019年現在、最終更新は2018年になっています。

まず、恋声で良いなと思う点です。

  • インストール不要で手軽に使える
  • 男声→女声の初期設定がある
  • 声の高さ(ピッチ)と声の性質(フォルマント)の設定の値の幅が広い
    • 自分の声質(低い声)でも女声っぽくできた

しかし、不満に思う点もあります。

  • 動作が不安定

これが私としては致命的な欠点であり、使用を躊躇する点です。

Gachikoe! Core

2019年現在、絶賛開発中のボイスチェンジャーです。

良いなと思う点は以下です。

  • インストール不要で手軽に使える
  • 設定項目が少なく簡潔
  • 低遅延、高音質らしい
  • 安定して動作する

長時間使用したわけではないので、安定して動作すると言っても恋声と比較しての話です。恋声は短時間の使用でも、動作が不安定になることが頻繁にありました。

恋声に思っていた致命的な欠点は改善されますが、再び致命的な欠点があります。

  • 設定の幅が狭い
    • 自分の声質(低い声)だと女声にはならない

今後に期待のボイスチェンジャーですが、現状では私のニーズを満たすものではありません。低遅延、高音質だとしても、作り出される声がそもそもニーズに満たないと仕方がなく、次へ。

バ美声

2019年現在、こちらも絶賛開発中のボイスチェンジャーです。

良いなと思う点は Gachikoe! Core と同じです。そして、欠点も同じです。Gachikoe! Core とそっくりなコンセプトだと思いました。

こちらも今後に期待のボイスチェンジャーですが、現状では私のニーズを満たすものではなく、次を試すことにしました。

RoVee

2019年現在、最終更新が2013年となっており既に更新が止まっている様子です。他の3つのソフトウェアは単体で動作するソフトウェアでしたが、RoVeeはVSTプラグインとなっており単体では動作しません。OBS StudioにはVSTプラグインによって音声変換を拡張する機能があります。RoVeeをOSB Studioに組み込んでの使用を考えました。

試してみたところ、OSB StudioでマイクのフィルタにRoVeeを表示できましたが、設定を行うウィンドウが表示されません。設定を変更できなければ使い物になりません。調べているとOSB StudioでRoVeeを使っている方もいる様ですが、設定の変更方法までは分かりませんでした。

OSB Studioで直接使うことは断念し、VSTプラグインを組み合わせて音声変換を行うVSTHostを使うことにしました。しかし、VSTHostの公式ダウンロードサイトが表示できません。非公式と思われるサイトは見つかりました。安全性に不安はありますが、下記のサイトからダウンロードできます。

VSTHostでRoVeeを読み込ませることで音声を変換できました。

この組み合わせの良いなと思う点は以下です。

不満に思う点と言えば下記くらい。

VSTHostは今後にメンテナンスされることがなく、使えなくなるかもしれないという不安はあります。その頃には、現在開発中のボイスチェンジャーが機能拡張されていることを祈ります。

まとめ

VSTHostを使えば複数のVSTプラグインを組み合わせて様々な変換を行えます。男声を女声にするだけでなく、複数の声を混ぜたり、エコーをかけたりと組み合わせ次第では無限です。あまり注意していませんでしたが、変換による遅延もそれほど感じませんでした。無料でありながら、安定して動作しつつ、カスタマイズの自由度があり自分の声にも対応できそうだと思います。

リモートデスクトップ接続でゲームコントローラーを使う

概要

ゲームマシンを遠隔で操作してプレイする方法です。ゲームコントローラーを使ってプレイします。

目次

確認環境

参考情報

解説

構成

構築した環境は次の図のとおりです。

ゲームマシンとクライアントマシンが WI-Fi で通信を行い、ゲームコントローラーとクライアントマシンが Bluetooth で通信を行っています。プレイヤーはクライアントマシンの画面を見ながらゲームコントローラーを操作します。しかし、ゲームコントローラーは Windows 用ですので macOS には接続できません。そのため、macOSVMware Fusion をインストールして Windows の仮想クライアントマシンを動かし、仮想クライアントマシンにゲームコントローラーを接続します。仮想クライアントマシンとゲームマシンはリモートデスクトップ接続します。つまり、ゲームコントローラーは、クライアントマシン (macOS) を経由して仮想クライアントマシン (Windows 10) に接続し、さらにリモートデスクトップ接続を経由してゲームマシンに接続します。

環境構築手順

ゲームコントローラーのドライバをインストールする

ゲームコントローラJC-U4113S のドライバをダウンロードしてインストールします。インストールする先は、仮想クライアントマシンとゲームマシンです。

クライアントマシンに Bluetooth のドングルを接続し、仮想クライアントマシン (WIndows 10) に接続させます。ゲームコントローラーは X Input で起動します。そうすると、コントロールパネルのデバイスとプリンターに JC-U4113S が表示されます。

リモートデスクトップ接続のポリシー設定を変更する

リモートデスクトップ接続でゲームコントローラーを共有するためにグループポリシーを編集します。編集は、仮想クライアントマシンとゲームマシンで行います。

gpedit を検索するとグループポリシーの選択が表示されます。

選択するとポリシーエディターが開きます。

コンピューターの構成 > 管理用テンプレート > Windows コンポーネント > リモートデスクトップサービスと辿って、次に示す項目を編集します。

編集を終えたら、コマンドプロンプトを管理者として実行して次に示すコマンドを実行します。

gpupdate /force

成功したら、マシンを再起動します。

ゲームコントローラーをゲームマシンに接続する

仮想クライアントマシンでリモートデスクトップ接続を行い、ゲームマシンに接続します。この際、オプションの表示 > ローカルリソース > 詳細 と開いていきます。リモートデスクトップ接続の設定が適切に行えていれば、ゲームコントローラーを仮想クライアントマシンに接続している状態で下記の様に XBOX 360 Controller が表示されます。

XBOX 360 Controller を選択した上で接続を開始します。ゲームマシンで、コントロールパネルのデバイスとプリンターに JC-U4113S が表示されていれば成功です。ゲームコントローラーの設定 > プロパティと辿れば、ゲームコントローラーのテストができます。

おわりに

私の環境では macOS に仮想クライアントマシンを構築して Windows 10 を動かしていますが、もちろん macOS は無くても構いません。Windows 10 のクライアントマシンからゲームマシンを操作する構成でも可能なはずです。

Android でメールを送信する

概要

メールクライアントアプリを使わずに、自身が開発したアプリでメールを送信する方法です。Google のサービスを使ってメールを送信します。1つの方法は GmailSMTP を使い、もう1つの方法は Google APIGmail サービスを使います。

目次

確認環境

  • macOS 10.13
  • AndroidStudio 3.2.1
  • Gradle 4.6
  • Kotlin 1.3.0

参考情報

解説

認証方法の比較

GmailSMTP を使う方法と Google APIGmail サービスを使う方法の両方ともに、認証が必要になります。認証方法には3つの方法があります。以下のとおりです。

  • GmailSMTP を使う場合
    • Gmail アカウントのパスワードで認証する
    • アプリパスワードで認証する
      • 2段階認証を有効にしている場合
  • Google APIGmail サービスを使う場合
    • Android の機能で認証して、OAuth2 で認可する

GmailSMTP をパスワード認証で使う(非推奨)

この方法は実装方法が最も簡潔な方法です。しかし、使わない方が良い方法です。なぜなら、ユーザーのセキュリティリスクを高めてしまうためです。詳しくは後述します。

まず、app モジュールの build.gradle に JavaMail for Android への依存を追加します。

dependencies {
    //...
    implementation 'com.sun.mail:android-mail:1.5.5'
    implementation 'com.sun.mail:android-activation:1.5.5'
}

AndroidManifest.xml に INTERNET の使用権限を追加します。

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

そして、メール送信を行う処理です。通信は Main (UI) スレッドで行えないため、AsyncTask を使っています。

package com.example.sendmail

import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage
import android.os.AsyncTask
import java.util.*
import javax.mail.*

class SendMail(
    private val fromAddress: String,
    private val password: String,
    private val toAddress: String,
    private val subject: String,
    private val text: String
) : AsyncTask<Unit, Unit, Unit>() {

    override fun doInBackground(vararg params: Unit?) {
        val props = Properties().apply {
            putAll(mapOf(
                "mail.smtp.host" to "smtp.gmail.com",
                "mail.smtp.socketFactory.port" to "465",
                "mail.smtp.socketFactory.class" to "javax.net.ssl.SSLSocketFactory",
                "mail.smtp.auth" to "true",
                "mail.smtp.port" to "465"
            ))
        }

        val session = Session.getDefaultInstance(props, object : Authenticator() {
            override fun getPasswordAuthentication() = PasswordAuthentication(fromAddress, password)
        })

        val message = MimeMessage(session).also {
            it.setFrom(InternetAddress(fromAddress))
            it.addRecipient(Message.RecipientType.TO, InternetAddress(toAddress))
            it.subject = subject
            it.setText(text)
        }

        Transport.send(message)
    }
}

Kotlin の文法に詳しくない方のための補足説明:
クラス名直後の () はコンストラクタの引数です。val と指定されているため、クラスのプロパティでもあります、プロパティは、フィールドと getter/setter を合わせたものです。Java におけるフィールドの定義と this.fromAddress = fromAddress; の様なコンストラクタの引数をフィールドに代入する処理を行っています。Java ではフィールド定義、コンストラクタ定義と分かれているところを Kotlin では1つにまとめて書けます。: AsyncTask で AsyncTask クラスを継承しています。継承すると共に、AsyncTask<...>() として AsyncTask クラスの引数なしコンストラクタを SendMail オブジェクトの生成時に呼び出しています。変数 props には Properties オブジェクトが代入されます。但し、Properties オブジェクトには apply 関数の {} 内の処理が適用されます。object : Authenticator() では Authenticator クラスを継承した無名クラスのオブジェクトを生成しています。MimeMessege(session).alsoMimeMessege(session).apply とほぼ同じなのですが、apply の場合は this.subject = subject となり、also の場合は it.subject = subject になります。apply の場合は subjectthis.subject が同じ変数を指してしまうため、ここでは also を使っています。詳しくは Reference - Kotlin Programming Language を参照してください。


最後に SendMail の呼び出しです。適宜、引数の内容は書き換えてください。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val task = SendMail(
            fromAddress = "your_gmail_account@gmail.com",
            password = "<your password>",
            toAddress = "your_friend_account@example.com",
            subject = "First Practice",
            text = "Hello World!"
        )
        task.execute()
    }
}

以上で完成です。アプリを実行させてみると、残念ながら LogCat に以下のエラーが表示されます。

javax.mail.AuthenticationFailedException: 534-5.7.14

エラーメッセージには https://support.google.com/mail/answer/78754 を見るように書かれています。読んでみると、安全性の低いアプリを許可する必要があることが分かります。許可は https://myaccount.google.com/lesssecureapps で行えます。有効に設定した上で、再びアプリを実行してみましょう。今度はメールが送信され、指定のアドレスに届いているはずです。届いていない場合は、パスワードが間違えていないか確認しましょう。

しかし、安全性の低いアプリの許可を有効にすると、様々な怪しいアプリにも許可を与えることになります。これは危険です。先程のヘルプには、2段階認証を設定し、アプリパスワードを使う方法があると書かれています。この方法は使わずに、アプリパスワードを使う方法を使います。先に進む前に、安全性の低いアプリの許可は無効に戻しておきましょう。

GmailSMTP をアプリパスワードで使う

では、2段階認証を有効にし、アプリパスワードを使ってメールを送信してみます。

https://myaccount.google.com/lesssecureapps のページの左向き矢印を選択すると、ログインとセキュリティの設定ページに移動します。パスワードとログイン方法の項目に、2段階認証プロセスの項目があります。手順に従って有効にしましょう。

手順どおりに進めて、次の画面が表示されたら2段階認証プロセスの設定は完了です。

左向き矢印を選択して、メニュー階層を戻りましょう。2段階認証プロセスを有効にすると、アプリパスワードの項目が追加されます。

アプリパスワードを追加します。メールを選択し、端末は Android が見当たらないのでその他を選択します。端末名の入力では Android とでも入力しておきます。

生成を行うと、黄色い枠のなかにアプリパスワードが表示されます。(黒塗りして隠しています。)

このアプリパスワードをコピーし、Gmail アカウントのパスワードと差し替えます。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val task = SendMail(
            fromAddress = "your_gmail_account@gmail.com",
            password = "<your application password>", // 差し替え
            toAddress = "your_friend_account@example.com",
            subject = "First Practice",
            text = "Hello World!"
        )
        task.execute()
    }
}

アプリを実行してみましょう。メールが送信され、指定のアドレスに届いているはずです。

Google APIGmail サービスを OAuth2 で使う

アプリパスワードを使う方法では、ユーザーにアプリパスワードの設定を行ってもらう必要があります。限られた環境のみで使うのであれば、手軽な方法ではあります。多くのユーザーが使うことを想定すると、使い勝手の観点から現実的ではありません。そこで、Android の機能で認証を行い、Google APIGmail サービスを OAuth2 で認可する仕組みを使います。

まずは、app モジュールの build.gradle にライブラリへの依存を追加します。

dependencies {
    // ...
    implementation 'com.sun.mail:android-mail:1.5.5'

    implementation('com.google.android.gms:play-services-auth:16.0.1') {
        exclude group: 'com.android.support'
    }
    implementation 'com.google.api-client:google-api-client-android:1.25.0'
    implementation 'com.google.api-client:google-api-client-gson:1.25.0'
    implementation 'com.google.apis:google-api-services-gmail:v1-rev96-1.25.0'
}

android-mail は無くても構いませんが、使うと Message (後述) の構築が楽です。play-services-auth では com.android.support を除外しています。Support Library への依存を別途追加する場合に、バージョンの違いでエラーになるため除外しています。

AndroidManifest.xml には INTERNET の使用権限を追加します。

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

そして、メール送信を行う処理です。通信は Main (UI) スレッドで行えないため、ここでも AsyncTask を使っています。

package com.example.sendmail

import android.content.Intent
import android.os.AsyncTask
import com.google.api.client.extensions.android.http.AndroidHttp
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.google.api.client.json.gson.GsonFactory
import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64
import com.google.api.services.gmail.Gmail
import com.google.api.services.gmail.model.Message
import java.io.ByteArrayOutputStream
import java.util.*
import javax.mail.Session
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage

class SendMailUsingGmailService(
    private val credential: GoogleAccountCredential,
    private val message: MimeMessage,
    private val onAuthError: (Intent) -> Unit
) : AsyncTask<Unit, Unit, Intent?>() {

    companion object {
        private fun createMimeMessage(
            fromAddress: String,
            toAddress: String,
            subject: String,
            text: String
        ): MimeMessage {
            val session = Session.getDefaultInstance(Properties())
            return MimeMessage(session).also {
                it.setFrom(InternetAddress(fromAddress))
                it.addRecipient(javax.mail.Message.RecipientType.TO, InternetAddress(toAddress))
                it.subject = subject
                it.setText(text)
            }
        }
    }

    constructor(
        credential: GoogleAccountCredential,
        toAddress: String,
        subject: String,
        text: String,
        onAuthError: (Intent) -> Unit
    ) : this(
        credential,
        createMimeMessage(credential.selectedAccountName, toAddress, subject, text),
        onAuthError
    )

    override fun onPostExecute(result: Intent?) {
        result?.also(onAuthError)
    }

    override fun doInBackground(vararg params: Unit?): Intent? {
        return try {
            buildService(credential).send(message)
            null
        } catch (e: UserRecoverableAuthIOException) {
            e.intent
        }
    }

    private fun buildService(credential: GoogleAccountCredential): Gmail {
        val transport = AndroidHttp.newCompatibleTransport()
        return Gmail.Builder(transport, GsonFactory(), credential)
            .setApplicationName("Send Mail")
            .build()
    }

    private fun Gmail.send(mimeMessage: MimeMessage) {
        users()
            .messages()
            .send(credential.selectedAccountName, createMessage(mimeMessage))
            .execute()
    }

    private fun createMessage(mimeMessage: MimeMessage): Message {
        return Message().apply {
            val bytes = ByteArrayOutputStream()
                .also(mimeMessage::writeTo)
                .toByteArray()
            raw = Base64.encodeBase64URLSafeString(bytes)
        }
    }
}

Kotlin の文法に詳しくない方のための補足説明:
クラス名直後の () はプライマリコンストラクタの引数です。SendMailUsingGmailService クラスには2つのコンストラクタがあります。constructor で始まる部分はセカンダリコンストラクタと呼ばれ、this(...) でプライマリコンストラクタを呼び出しています。companion object の {} 内に関数を書くとクラスメソッドとして振る舞います。result?.also(onAuthError) は、result が null では無いときのみ onAuthError(result) を実行する書き方です。? を書かない場合は null チェックを行わずに onAuthError(result) を実行します。try-catch 構文は try, catch のブロック内の最後の値が try-catch 式の値となって、その値が return により関数の戻り値にされます。具体的には、UserRecoverableAuthIOException が発生した場合は e.intent が戻り値となり、成功した場合は null が戻り値になります。fun Gmail.send(...) は拡張関数と呼ばれ、Gmail クラスに send メソッドが追加された様に見せかけることができます。そのため、buildService(credential).send(message) という書き方ができています。buildService の戻り値は Gmail オブジェクトですので、拡張関数である send メソッド(関数)を呼び出せる様になっています。 詳しくは Reference - Kotlin Programming Language を参照してください。


さて、本題に戻って処理の内容です。Google アカウントで認証した結果が GoogleAccountCredential として渡されます。メールのコンテンツは MimeMessage として扱われます。buildService 関数では Gmail サービスを準備し、send 関数でサービスを利用しています。createMessage 関数では MimeMessage をバイト列に変換して、 API に送信するメッセージ本文の raw にそのバイト列を設定しています。

SendMailUsingGmailService の呼び出しは Google アカウントを使った認証と OAuth2 による認可の手順が必要になるため複雑になります。

package com.example.sendmail

import android.accounts.AccountManager
import android.app.Activity
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential
import com.google.api.services.gmail.GmailScopes

class MainActivity : AppCompatActivity() {

    companion object {
        private const val REQUEST_ACCOUNT_CHOOSER = 1
        private const val REQUEST_AUTHORIZATION = 2
    }

    private lateinit var mCredential: GoogleAccountCredential

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mCredential = GoogleAccountCredential.usingOAuth2(this, listOf(GmailScopes.GMAIL_SEND))
        startActivityForResult(mCredential.newChooseAccountIntent(), REQUEST_ACCOUNT_CHOOSER)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {
            REQUEST_ACCOUNT_CHOOSER -> {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
                    Log.i(localClassName, "Account Name: $accountName")
                    if (accountName != null) {
                        mCredential.selectedAccountName = accountName
                    }
                    sendMail()
                }
            }
            REQUEST_AUTHORIZATION -> sendMail()
        }
    }

    private fun sendMail() {
        val task = SendMailUsingGmailService(
            mCredential,
            toAddress = "your_friend_account@example.com",
            subject = "First Practice",
            text = "Hello World!"
        ) {
            startActivityForResult(it, REQUEST_AUTHORIZATION)
        }

        task.execute()
    }
}

Kotlin の文法に詳しくない方のための補足説明:
val は再代入不可な変数、var は再代入可能な変数です。lateinit は後で初期化することを示す印です。lateinit を使わない場合には var mCredential: GoogleAccountCredential? = null とします。変数の型に ? が付く場合には、変数に null の代入を許容することになるので変数を使う時に null チェックが必要になります。変数を使う前に必ず null 以外で初期化され、変数を使う時の null チェックを減らしたい場合に lateinit を使います。SendMailUsingGmailService のコンストラクタの引数には onAuthError が必要です。しかし、一見すると () 内には引数が見当たりません。Kotlin では、最後の引数の型が関数である場合 () の外側に記述できます。{} はブロック(複数の文をまとめる構造)、もしくはラムダ式(関数の一種)を表しており、{ startActivityForResult(...) } は関数の一種ということになります。onAuthError 引数にはこの関数が代入されます。詳しくは Reference - Kotlin Programming Language を参照してください。


GmailScopes.GMAIL_SEND を指定して、送信サービスの認可を得ようとしています。startActivityForResult(..., REQUEST_ACCOUNT_CHOOSER) ではアカウントの選択を起動します。アカウントを選択後 onActivityResult の REQUEST_ACCOUNT_CHOOSER の部分が実行されます。選択されたアカウント名を GoogleAccountCredential に設定しています。これにより GoogleAccountCredential が整ったので、メールの送信処理に移ります。メールを送信しようとすると、サービスへのアクセス許可が与えられていない場合に onAuthError コールバックが呼び出されます。コールバックでは startActivityForResult(it, REQUEST_AUTHORIZATION) を呼び出してアクセス許可を与えるかの選択をユーザーに促します。選択の後、onActivityResult の REQUEST_AUTHORIZATION の部分が実行されます。再びメールを送信し、アクセス許可が与えられていたら onAuthError コールバックは呼び出されずに終了します。

以上で、実装は完了しました。アプリを起動してみます。しかし、ビルドの途中で以下のエラーが表示されるかもしれません。

More than one file was found with OS independent path 'META-INF/DEPENDENCIES'

この場合には、build.gradle に以下を追加すれば解決します。(理由は調べられていません。どなたか教えて頂けると嬉しいです。)

android {
    // ...
    packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
    }
}

アプリが起動するとアカウント選択が表示され、いずれかのアカウントを選択します。

そうすると、エラーが発生します。LogCat に以下のエラーが表示されます。

com.google.android.gms.auth.GoogleAuthException: UNREGISTERED_ON_API_CONSOLE

解消するためには Developer Console でサービスを有効化しておく必要があります。

Developer Console でサービスを有効化

まず、Google Cloud Platform から Console を開きます。既存のプロジェクトでサービスを有効化しても良いですが、ここでは新しいプロジェクトを作成します。プロジェクト名は何でも良いので、ここでは SendMail にしておきます。

プロジェクト (SendMail) が選択されていることを確認した上で、API とサービスのメニューを開きます。ここから「API とサービスの有効化」を選択します

Gmail を検索し、Gmail API を選択します。

Gmail API を有効にします。

次に、Gmail API の認証情報を設定します。認証情報メニューを選択し、「認証情報を作成」を選択します。

使用する APIGmail APIAPI を呼び出す場所は Android、アクセスするデータの種類はユーザーデータです。

「必要な認証情報」を選択すると次のステップに進みます。

クライアント ID を作成するにあたっての名前は後で見て分かる名前を付けます。署名証明書フィンガープリントは Terminal で keytool コマンドを使って取得します。

書かれているとおりのコマンドを実行しますが、path-to-debug-or-production-keystore は自分で埋める必要があります。debug-keystore を使う場合は以下のとおりに実行します。(macOS の場合です。)

$ keytool -exportcert -keystore ~/.android/debug.keystore -list -v

パスワードを求められますが、何も入力せずに Enter で先に進めます。表示された情報のなかから、SHA1 のフィンガープリントをコピーします。Android アプリを識別するための情報(指紋)です。配布するアプリの場合には、自分で作成した production-keystore ファイルのパスを指定します。今回は実験用なので debug-keystore を使っています。

パッケージ名は AndroidManifest.xml で指定したパッケージ名を記入します。しかし、build.gradle の applicationId で異なるパッケージ名を設定している場合には applicationId を記入する必要があるようです。私は試していませんが StackOverflow でみかけました。なお、1つの Android アプリを複数のプロジェクトには設定できない様です。異なるアカウントの異なるプロジェクトに同一アプリを設定しようとしたところ、設定できませんでした。

記入を終えて「OAuth クライアント ID を作成」を選択すると次のステップに進みます。

次は、ユーザーに表示される同意画面の設定です。ユーザーに見せるサポート用のメールアドレス、サービス名を設定します。今回は API を使うサービスが Android のアプリだけなので Android アプリの名前を設定しています。サービス名が別途存在する場合には、そちらを設定すれば良いでしょう。

「次へ」を選択すると最後のステップに進みます。Android アプリで使う場合には、認証情報をダウンロードしなくて構いません。完了を選択します。

次に、OAuth 同意画面を設定します。アプリケーション名、サポートメールは先程設定した内容が表示されます。

下にスクロールすると Google API のスコープの設定が現れます。ユーザーにアクセスを許可する範囲の設定です。「スコープを追加」を選択し、gmail.send を探して追加します。

更に下にスクロールすると保存ボタンがあります。個人的な利用の範囲であれば保存で構いません。しかし、一般に公開したい場合には「確認のため送信」を選択する必要があります。公開の前に Google による確認が行われます。今回は省略していますが、プライバシーポリシーの設定などの他の項目も適切に設定した上で確認を依頼しましょう。

以上でサービスの有効化は完了しました。アプリを起動して試してみましょう。Google アカウントの選択までは既に行えていました。

アカウントを選択するとエラーではなく、同意画面が表示されます。

許可を選択することで同意したことになり、gmail.send (GmailScopes.GMAIL_SEND) が有効になります。これで、Gmail サービスを使ったメール送信が行える様になります。

おつかれさまでした。

無料で使えるプライベート Git リポジトリを共有する

概要

Google Cloud Platform のサービスの一つである Cloud Source Repositories を使って、プライベート Git リポジトリを共有する方法です。Cloud Source Repositories には、リポジトリ数制限の無い無料枠があります。この無料枠を使ってプライベート Git リポジトリを共有します。

目次

確認環境

  • 2018.11.07 現在の Google Cloud Platform

参考情報

解説

Cloud Source Repositories の無料枠

Cloud Source Repositories は、3 つの制限の範囲内では無料になります。

1 つめの制限はユーザー数です。5 ユーザーまでが無料になっています。より厳密に言うと、5 プロジェクトユーザーと書かれています。プロジェクト当たりではなく、あるユーザーが 2 プロジェクトでサービスを使用した場合は 2 プロジェクトユーザーとカウントする様です。ユーザー数のカウントは、ユーザーにアクセス権限を与えた時点で行われるのではなく、リポジトリにアクセスした時点で行われると書かれています。

残りの 2 つは、ストレージ容量の 50GB までという制限と、通信容量の下り 50GB/月 までという制限です。

リポジトリの作成

まずは、管理者権限を持つ Google アカウントでログインし、 Google Cloud Platform のページを開きます。そして、プロジェクトを選択します。(プライベートリポジトリなので諸々が黒塗りになっています。)

プロジェクトが存在しない場合は「新しいプロジェクト」を選択してプロジェクトを作成します。作成したプロジェクトが一覧に表示されますので、プロジェクトを選択します。

プロジェクトが選択された状態で(黒塗り部分)、ツールにある「Source Repositories」を選択します。そこからさらに、「リポジトリ」を選択します。(例では、ピン止めしているので Source Repositories が上にも表示されています。)

リポジトリの一覧が表示される画面になります。この画面で「リポジトリを作成」を選択し、指示に従ってリポジトリを作成します。作成が済むと例の様に一覧に表示されます。(例ではリポジトリを 2 つ作っています。)

アクセス権限の設定

アクセス権限は役割を設定することで行います。役割は複数のアクセス権限を集約しています。

ここでは、リポジトリにアクセスするために 2 つの役割を設定します。1 つはプロジェクトを閲覧する権限を持つ役割です。メンバーがプロジェクトを検索できる様にするために付与します。もう 1 つはリポジトリの操作権限を持つ役割で、読み込みのみか、書き込みも行うかで異なる役割を付与します。

役割の設定は IAM から行います。

IAM の設定画面で「追加」を選択してメンバーを追加します。メンバーに役割を設定して追加すると、一覧に役割が表示されます。

メンバーを追加するときには、プロジェクトに対する役割として閲覧者を選択します。

同様に、ソースに対する役割として Source Repository 読み取りか Source Repository 書き込みを選択します。詳しくは、役割と権限のマトリックス に書かれています。

リポジトリにアクセスする

ここからは追加したメンバーのアカウントでログインして操作します。

まず、プロジェクトを選択します。プロジェクト一覧に表示されない場合には、検索します。プロジェクトに対して閲覧者の役割が与えられていれば表示されます。

プロジェクトが選択された状態で、サービスの一覧から Source Repositories を選択します。そこから、リポジトリを選択します。

リポジトリの一覧が表示されます。一覧の中から、ソースコードを取得したいリポジトリのクローンを選択します。

そうするとリポジトリをクローンするための手順が表示されます。手順に従って行えばクローンされます。デフォルトは Google Cloud SDK を使った手順が表示されますが、Google Cloud SDK をインストールしたくない場合には「手動で生成した認証情報」を選択します。

RecyclerView の各項目で、スライドしてメニューを表示する

概要

以下の動画の様に、RecyclerView の各項目で、スライドしたときにメニューを表示させる方法です。他のライブラリを導入せずに、ItemTouchHelper クラスを使って実現します。

サンプルでは削除ボタンしかありませんが、複数のボタンを設定できます。また、ドラッグ&ドロップにより、項目の入れ替えができます。

目次

確認環境

  • Kotlin 1.2.71
  • AndroidStudio 3.2.1
  • minSdkVersion 16
  • compileSdkVersion 28

参考情報

解説

RecyclerView の設定

まず、RecyclerView と RecyclerView.Adapter を用意します。注意するポイントだけ記載しておきます。

  • Recycler View を定義した Activity のレイアウト XML
    • RecyclerView の id を content にしている。
  • ViewHolder が保持する View のレイアウト XML
    • background の View (ConstraintLayout) の id を background に、foreground の View (CardView) の id を foreground にしている。
    • ConstraintLayout を使って foreground と background を重ねて表示している。
      • ConstraintLayout を使わなくても重ねて表示できれば他のレイアウトでも構わない。
      • foreground を上に表示するため、foreground をファイル内の下方で定義している。
    • View の高さは background に合わせている。
      • foreground に合わせることもできる。(詳しくはソースコード内のコメントを参照)
      • ConstraintLayout における android:layout_height="0dp" は match_constraint を意味する。
  • Activity のコード
    • Kotlin Android Extensions の View Binding によって、RecyclerView が変数 content に 設定されている。
    • ItemTouchHelper と RecyclerView を結びつけている (ItemMotionHandler は後述)
  • RecyclerView.Adapter のコード
    • ViewHolder から Adapter のプロパティを参照するために、ViewHolder は inner class にしている。
    • ItemMotionHandler (後述) を使うために、LayeredViewHolder を継承している。
      • LayeredViewHolder の抽象プロパティである foreground, background を実装している。
    • ViewHolder では、Kotlin Android Extensions の View Binding によって、変数 textView, buttonDelete などに各 View を設定している。

ItemMotionHandler

今回のサンプルの肝は ItemMotionHandler.kt です。 ItemTouchHelper.Callback を実装したクラスになっています。

移動中の表示処理

特に重要な部分は onChildDraw メソッドのオーバーライドです。各項目を操作(上下移動、左右移動)したときの表示を制御しています。制御の切り分けは、dX の値によって行っています。dX は、移動していない基準の位置からの移動量です。

dX が 0 になるのは、横移動が完了した時と、縦移動をしている最中です。これらの場合には、onDraw に onChildDraw の引数の値をそのまま渡すことで、ItemTouchHelper の標準動作を行っています。

右方向に動かす場合は、dX > 0 になります。この場合、background は移動せず、foreground だけを移動します。そのため、onDraw の x が異なっています。(dY は 0 になるので同じ)。foreground の x は background の横幅を超えない様になっています。

左方向に動かす場合は、dX < 0 になります。左に動かす必要があるのは、メニューが開いている項目だけです。mLockedForeground には、メニューが開いている項目の foregroud の View が設定されています。foreground の View を比較することでメニューが開いている項目かを判定し、移動が不要であればメソッドを終了します。左に移動させる場合には、foreground だけを移動させます。そのため、右方向同様に onDraw の引数が異なっています。x の範囲を background の範囲を超えない様にする点も同様です。

但し、ここで一つ注意があります。左方向に移動したときに dX < 0 となるのは、onSwiped メソッド内で mItemTouchHelper.onChildViewDetachedFromWindow を行っているためです。この処理が無い場合、左方向の移動の場合でも dX > 0 になります。このあたりの理由は、onChildDraw の長いコメントを参照してください。

メニュー表示の解除

mLockedForeground には、メニューが表示されている項目の foreground の View が設定されています。これを使って、メニュー表示の解除を実現しています。

他の項目のメニューを表示する場合、項目を上下方向に移動する場合、メニューを閉じる場合には mLockedForeground に null を設定して、表示を基準の位置に戻します。これらは、unlockForeground メソッドの呼び出しや、onSwiped メソッド内の direction が ItemTouchHelper.START の場合の処理で実現しています。他の項目のメニューを表示する場合と項目を上下方向に移動する場合は、強制的に表示を元に戻すために clearView メソッドを呼び出しています。onSwiped メソッドが呼び出された場合には表示位置が基準の位置に戻っているので、clearView の呼び出しはしていません。

foreground.addOnLayoutChangeListener(mUnlockForeground) は、RecyclerView をスクロールした時に foreground を元に戻すために行っています。設定しないと、foreground がリサイクルされた他の項目でメニューが表示されます。

ドラッグ&ドロップ

ドラッグの開始位置とドロップ位置を通知する仕組みを用意しています。onDrop プロパティにリスナーオブジェクトを設定するとドラッグ&ドロップの機能が有効になり、ドロップした時点でリスナーが呼び出されます。

onMove メソッドで notifyItemMoved メソッドを呼び出すと RecyclerView の項目が入れ替わります。onMove は項目の位置が一つ入れ替わる毎に呼び出されます。ドラッグの開始位置とドロップ位置を取得したい場合には、不都合です。一方、onSelectedChanged メソッドは、上下左右の移動が開始/終了したタイミングで呼び出されます。actionState が 2 の場合は、上下移動の開始を意味しています。上下移動が開始した時点で mMoveFrom を設定し、移動終了 (actionState が 0 になった) タイミングで onDrag のリスナーを呼び出します。左右移動が終了した場合には、mMoveFrom が null になっているため onDrag は呼び出されません。

ItemMotionHandler の生成

ItemMotionHandler のインスタンスは newInstance メソッドで生成します。コンストラクタは使えません。

ItemMotionHandler は mItemTouchHelper.onChildViewDetachedFromWindow を行うために ItemTouchHelper のオブジェクトを保持する必要があります。ItemTouchHelper はコンストラクタに ItemTouchHelper.Callback (このサンプルでは ItemMotionHandler) を渡す必要があり、ItemMotionHandler のコンストラクタで ItemTouchHelper のオブジェクトを受け取ることができません。コンストラクタでの設定ができないため ItemTouchHelper を var にしたプロパティに設定しています。しかし、ItemTouchHelper を可変のままで public に公開するのは危ないため、プロパティ mItemTouchHelper は private var とし、newInstance メソッドで ItemTouchHelper と ItemMotionHandler を生成して関連づけています。ItemMotionHandler の生成は newInstance メソッドに限定したいため、ItemMotionHandler のコンストラクタは private にしています。

ItemTouchHelper の onChildViewDetachedFromWindow メソッドを呼び出す方法は、ItemTouchHelper の実装が変更された場合の影響を受けるため危険ではありますが、コード量をできる限り減らして実現する方法を模索した結果としての妥協案です。他にもっと良い実装がある場合には教えてください。

Android の ImageView.ScaleType

概要

ImageView.ScaleType を解説している記事は見かけますが、表で整理された内容を見かけないため整理します。

目次

参考情報

解説

ScaleType 縦横比 配置 はみ出し 隙間 サイズ
CENTER 維持 中央 両方向 元サイズ
CENTER_CROP 維持 中央 拡大のみ
CENTER_INSIDE 維持 中央 両方向 縮小のみ
FIT_CENTER 維持 中央 片方向 拡大/縮小
FIT_END 維持 右下 片方向 拡大/縮小
FIT_START 維持 左上 片方向 拡大/縮小
FIT_XY 無視 中央 拡大/縮小
MATRIX - - - - -

縦横比に注目すると、FIX_XY のみが縦横比を無視して拡大/縮小します。他は、縦横比を維持します。

配置は、基本的に中央寄せです。FIT_END と FIT_START のみが中央以外に寄せます。日本では、END が右下、START が左上です。日本以外では、START が右になる場合もあるので、表の内容は日本の場合です。

CENTER は、元サイズのままであるため、View に収まらない範囲は View からはみ出ますし、View の範囲よりも小さい場合には View の枠と画像の間に隙間ができます。隙間は、縦と横の両方向にできる場合があります。

CENTER_CROP は、はみ出した部分を切り取ります。View の範囲より小さい場合には拡大するため、View の枠と画像の間に隙間ができません。

CENTER_INSIDE は、はみ出さないように縮小します。View の範囲より小さい場合には元サイズのままとなるため、View の枠と画像の間に隙間ができます。隙間は、縦と横の両方向にできる場合があります。

FIT_CENTER は、はみ出さない範囲で拡大/縮小します。縦と横のいずれかの方向が View の枠の幅と同じになるまで拡大します。そのため、隙間は、片方向だけにできます。FIT_END、FIT_START も同様です。

Spring WebFlux で InMemoryWebSessionStore にセッションタイムアウトを設定する

概要

Spring WebFlux を使いながら application.properties で spring.session.timeout を設定した際、設定が効きませんでした。WebFlux の WebSessionStore をカスタマイズして、セッションタイムアウトの間隔を設定する方法を説明します。

目次

確認環境

  • Spring Boot 2.0.3.RELEASE
  • Kotlin 1.2.50

参考情報

解説

設定方法

以下の示す様に WebSessionManager に設定する WebSessionStore を変更します。

@EnableWebFluxSecurity
@EnableConfigurationProperties(SessionProperties::class)
@Configuration
class ExampleConfig {
    // ...

    @Bean
    fun webSessionManager(properties: SessionProperties): WebSessionManager {
        return DefaultWebSessionManager().apply {
            sessionIdResolver = HeaderWebSessionIdResolver().apply {
                headerName = "X-Sample" // セッションIDのヘッダー名を変更する場合には設定
            }
            sessionStore = CustomizableInMemoryWebSessionStore(properties)
        }
    }
}

WebSessionManager としては DefaultWebSessionManager を使います。DefaultWebSessionManager では、WebSessionIdResolver と WebSessionStore を指定します。デフォルトは、CookieWebSessionIdResolverInMemoryWebSessionStore です。

セッションタイムアウトには、WebSessionStore が関係しています。WebSessionStore がセッションの生成と破棄を行っています。デフォルトの InMemoryWebSessionStore では、InMemoryWebSession オブジェクトの maxIdleTime で有効期間の判定を行っています。デフォルトの maxIdleTime は 30 分になっています。

WebSessionStore に application.properties の設定内容を渡すために、EnableConfigurationProperties を使って SessionProperties を Bean として設定しています。これにより、webSessionManager メソッドの引数で SessionProperties Bean を注入できる様になります。SessionProperties は application.properties のうち、spring.session に含まれる設定内容を保持しています。InMemoryWebSessionStore 自体は SessionProperties を受け取らないため、InMemoryWebSessionStore を拡張したクラスのオブジェクトを使います。

本筋からは逸れますが、WebSessionIdResolver はリクエストとレスポンスにおいてセッションIDの取得と設定を行う処理を持ちます。HeaderWebSessionIdResolver を使えば、セッションIDを設定するヘッダーを変更できます。上記の例では、X-Sample ヘッダーにセッションIDを設定させています。

次に、InMemoryWebSessionStore を拡張した CustomizableInMemoryWebSessionStore です。

class CustomizableInMemoryWebSessionStore(
        private val properties: SessionProperties
) : InMemoryWebSessionStore() {

    override fun createWebSession(): Mono<WebSession> = super
            .createWebSession()
            .map(this::applySessionProperties)

    private fun applySessionProperties(session: WebSession) = session.apply {
        maxIdleTime = properties.timeout
    }
}

WebSession オブジェクトを生成する際に、maxIdleTime を application.properties の spring.session.timeout の値に変更しています。