「きのこの山」を「たけのこの里」に『正しく』自動で修正して差し上げるプログラム（Androidアプリ版）

## 「たけのこの里」という非常に素晴らしいお菓子
人類であれば皆**「たけのこの里」**という非常に素晴らしいお菓子を食べたことがあるはずです。長さ2cmほどのたけのこ状のお菓子で、クッキーの生地の表面に程よい口溶けのチョコレートが二重にわたってコーティングされています。生地のクッキーは丁寧に形を整えながら焼くという「型焼」という当時日本初の手法を取っており、**明治様がどれほどの愛情を「たけのこの里」に注いでいるのか**がよくわかります。

しかしながら、たけのこの里という素晴らしいお菓子が発明される前に、明治様はとんでもない間違えを犯してしまいます。**「きのこの山」**という**完全出来損ないの商品**が発売されていたのです。たけのこの里に比べて**ポリポリと固く質感の悪いクラッカー**の先端に**適当にチョコを乗せただけ**という、なんとも言えないお菓子となっています。味も売り上げも旨さも人気さも全てにおいて「たけのこの里」のほうが1枚...いや**300枚上手**なのですが、悲しいことに**姉妹品**という括りになってしまっています。「たけのこの里」の爆発的ヒットの裏側で人気がガタ落ちしていた「きのこの山」の売り上げを伸ばすべく、姉妹品という言葉を使って「たけのこの里」にあやかろうとしたのでしょう。当時の担当者様も**高位の存在である**「たけのこの里」を「きのこの山」と同等の存在であるかのように宣伝するのは**苦渋の決断**だったでしょう。心中お察しいたします。

しかしその影響でか、ネット上では**「きのこたけのこ戦争」**という**本来であれば起こるはずのない争い**が生まれてしまっています。どういう理由か理解に苦しみますが、「きのこの山の方がおいしい」という**妄言**をおっしゃる方々がいらっしゃるのです。**無知というのは非常に恐ろしい**ものですね。事実、国民総選挙でもたけのこの里が**二度にわたって大金星**をあげています。テレビ朝日の『日本国民がガチで投票! お菓子総選挙2016』という番組ではたけのこの里が8位でしたが、きのこの山は**ランク外**という惨敗に終わっています。さらに、あの**日本国民の英雄かつ霊長類最強とまで言われた「吉田沙保里」様もたけのこの里を支持[^1]**しています。

このような事実を正しく理解していれば、もはや**争いなど起こる余地はない**のですが、ネット上では面白がってか、この2製品の間にライバル関係があるかのような発言をする方がいます。たけのこの**絶対的正義は揺るぎない**のであまり気にする必要もないとは思うのですが、新参の方があらぬ間違いを起こしてしまう可能性も皆無ではありません。

このQiitaでも[「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB-%E7%B4%A0%E6%99%B4%E3%82%89%E3%81%97%E3%81%84%E3%81%8A%E8%8F%93%E5%AD%90%E3%81%AE%E7%B4%B9%E4%BB%8B%E3%81%A8%E5%BE%8C%E7%99%BA%E5%8A%A3%E5%8C%96%E5%93%81%E3%81%AE%E5%AD%98%E5%9C%A8) という記事がでていました。この記事の影響で「きのこの山派」と呼ばれる**事実を正しくとらえられない**方が増えてしまう可能性もありますので、今回は**「きのこの山」を「たけのこの里」に『正しく』自動で修正して差し上げるプログラム**を作りたいと思います。

## 正しく修正できた例
今回はAndroidアプリとしての開発となります。レイアウトも含め後ほど掲載しますが、まずは動作例をご覧ください。

### 修正例その１
例えばTwitterで以下のようなツイートを見かけたとします。

```
今度みんなで「きのこの山パーティー」しようぜ！
```

**かわいそうに**。おそらくこの若者はきのこの山という**時代遅れの製品**しか食べたことがないのでしょう。もしくは**若き日の間違い**というやつでしょうか。理由は何にせよ、このようなことは決してあってはなりません。この若者だけではなく、パーティーに参加した人々にまで汚染が広まってしまい、歴史誤認が拡大しかねません。ツイートした本人またパーティーの参加される方々の**正しい歴史認識を汚さないために**修正して差し上げることにしましょう。

```
修正済みテキスト：今度みんなで「たけのこの里パーティー」しようぜ！
```

**是非やってください**。何なら私も呼んでください。国はこのような事実を正しく認識し、広めていこうとする動きに補助金を出すべきです。

### 修正例その2
次にAmazonで以下のようなレビューを見たとします。

```
たけのこの里って、チョコは味しないし土台は崩れやすいから嫌い。☆1もつけたくない。
```

**意味が分からない**。きのこの山のように、ただ甘ったるいチョコを大量に乗せることがいいとでも思っているのでしょうか。薬剤師様の批評[^2]によると、きのこの山は**異性化液糖が含まれていて体に悪い**そうです。もしくは何かの変換エラーで誤って「たけのこの里」と打ってしまったのでしょうか。あり得る話です。人間ですから間違うこともあります。そのような場合はこのプログラムを適用させて、本来打ちたかった「きのこの山」に自動で変換してあげることにしましょう。

```
修正済みテキスト：きのこの山って、チョコは味しないし土台は崩れやすいから嫌い。☆1もつけたくない。
```

**その通りです**。たけのこの里は2種類のチョコレートを**絶妙なハーモニー**で掛け合わしているので、たけのこの里を食べた人は舌が肥えているのです。そのような人たちが**出来損ないであるきのこの山**を食べたところで、何の味もしなくて当たり前です。

### 修正例その3
次に以下のようなLINEが来たとします。

```
たけのこの里って本当においしいよな!
```

**世界常識です**。当たり前のことですが、何度も噛みしめるように言うのは良いことですよね。私もたけのこの里を食べるたびにこう思います。このような文章は**本来修正する必要はない**のですが、テストとして一度修正にかけてみましょう。「たけのこの里」を裏切るような行為に見えますが、これも「たけのこの里」のためを思ってのことです。

```
修正済みテキスト：たけのこの里って本当においしいよな!
```

何も変わりませんね。**これが世界常識**なのですから当たり前です。もし誤って「たけのこの里」を「きのこの山」に修正してしまうようなことがあれば、私は**日本で生活できなくなる**ことでしょう。プログラム通り動いてよかったです。

### 修正例その4
最後に、ネット掲示板で以下のようなコメントを見かけたとします。

```
たけのこの里マジで不味くてワロタwww
```

**言語道断です**。何がワロタなのか理解不能です。**ふざけるのも大概にしてほしい**ものです。このようなコメントは**青少年の育成に莫大な悪影響を及ぼす恐れ**があります。吉田沙保里様も支持[^1]している「たけのこの里」は**国家に認められたお菓子**といっても過言ではありません。そのような存在である「たけのこの里」を汚すような言葉は、名誉棄損罪や**国家転覆罪**(内乱罪)に当たる可能性があります。この一言を投稿した人が罪に問われないように、修正して差し上げることにしましょう。

```
修正済みテキスト：きのこの山マジで不味くてワロタwww
```

あまり1つの製品をやり玉に挙げて批判することは良くないと思いますが、とりあえず投稿者の方が罪に問われる可能性はなくなりました。よかったです。

## 仕組み
健全な方であれば、この時点でLGTMやTwitterへのシェアなどを行っていると思いますが、一応仕組みとコードについて解説します。

仕組み自体は[「たけのこの里」を「きのこの山」を『正しく』自動で修正して差し上げるプログラム](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB-%E7%B4%A0%E6%99%B4%E3%82%89%E3%81%97%E3%81%84%E3%81%8A%E8%8F%93%E5%AD%90%E3%81%AE%E7%B4%B9%E4%BB%8B%E3%81%A8%E5%BE%8C%E7%99%BA%E5%8A%A3%E5%8C%96%E5%93%81%E3%81%AE%E5%AD%98%E5%9C%A8) の仕組みと同じです。こちらの記事も併せてご覧ください。しかしながら、多少**「たけのこの里」を貶すような発言**が見られますので、皆さん注意してご覧ください。

上記の修正例をご覧になってお分かりかと思いますが、『正しく』修正するということは、ただ「たけのこの里」と「きのこの山」を入れ替えればいいということではありません。修正例その1のように**何故か「きのこの山」を誉めている**文章の場合は不適切ですので、**本来称賛されるべき「たけのこの里」**と入れ替えて差し上げます。逆に「たけのこの里」を褒め称えいる文章の場合は、**それが当たり前**ですので修正は実行しません。詰まる所、以下のような基準となります。

**称賛している（ポジティブ）＋「きのこの山」が含まれている**といった文章の場合は修正を実行します。
**貶している（ネガティブ）＋「たけのこの里」が含まれている**といった文章の場合、修正は実行しません。

さらに、ただ単に「たけのこの里」「きのこの山」という単語を検索するわけではありません。例えば以下のような文章をご覧ください。

**「火事のときのこの山は危険だ」** (火事の時の、この山は危険だ)
**「あの娘に首ったけのこの里見少年」** (あの子に首ったけの、この里見少年)

このような文章の場合「たけのこの里」という商品とは無関係ですので、誤って修正してしまうのは**文章を書いた方に失礼**ですし、何より**「たけのこの里」への冒涜**にあたります。ですので、「たけのこの里」「きのこの山」という**固有名詞**が文章に含まれているかを調べる必要があります。

今回は、ネガポジ判定に[Microsoft Azure](https://azure.microsoft.com/ja-jp/)と[Google 翻訳](https://translate.google.com/)を利用します。Google翻訳を利用するのは、日本語のままだとネガポジ判定の精度が落ちるからです。固有名詞の抽出には[COTOHA 固有表現抽出 API](https://api.ce-cotoha.com/contents/api-all.html)を利用します。

注：本来であれば、主語を推定し、ネガポジがどこの単語までかかっているのか係り受け分析すべきですが、今回は省略させていただきます。

## 実装
### 環境
今回はより多くの方に**正しい歴史認識**を持ってもらうために、シェア率の高いAndroidアプリとして開発します。Androidアプリとして開発することで、日本国内だけでなく外国に対しても「たけのこの里」が**いかに高位の存在であるか**を知らしめることができます。余談ですが、外国人にも「たけのこの里」が絶対的正義であることは僅かながら浸透しつつあるようです。[^3](注釈参照)**老若男女皮膚の色問わず「たけのこの里」がおいしいということでほぼ一致**しましたね。喜ばしいことです。

さて、環境はAndroidStudio4.0 ac1で開発していきます。HTTPSクライアントライブラリは[Fuel](https://fuel.gitbook.io/documentation/core/fuel)を採用します。Jsonパーサーは[Google Gson](https://github.com/google/gson)と[Klaxon](https://github.com/cbeust/klaxon)で迷いましたが、今回は[Klaxon](https://github.com/cbeust/klaxon)を採用することにしました。Fuelについては「[Kotlin/FuelでHTTPアクセスしました。](https://qiita.com/naokiur/items/d6337bfc976cb1249575)」をご覧ください。Klaxonについては「[KotlinとKlaxsonによるJSONの処理](https://www.codeflow.site/ja/article/kotlin-json-klaxson)」をご覧ください。

ライブラリを決定したら、Gradleに以下のように記述します。

<details><summary>以下コード（クリックして展開）</summary><div>

```Gradle
dependencies {
    implementation 'com.beust:klaxon:5.0.1'

    implementation 'com.github.kittinunf.fuel:fuel:1.12.0'
    implementation 'com.github.kittinunf.fuel:fuel-android:1.12.0'
}
```

</div></details>

### Google翻訳 API
そもそもGoogle翻訳 APIは有料です。500,000文字までは一応無料で使うことができますが、世界中のありとあらゆる「きのこの山」を「たけのこの里」へと修正していたらあっという間に上限が来てしまいます。どうしたものかと困り果てていたところ、[Google翻訳APIを無料で作る方法](https://qiita.com/satto_sann/items/be4177360a0bc3691fdf)という記事を見つけました。こちらの記事をもとにAPIを作って利用します。

<details><summary>以下コード（クリックで展開）</summary><div>

```Kotlin
    private val googleTranslateUrl = "作成したAPIのエンドポイント"

    private fun requestTranslate(text: String): String?{

        //自作したGoogle翻訳APIにリクエストを投げる
        val (_, _, result) = "$googleTranslateUrl?text=$text&source=ja&target=en".httpGet().responseJson()

        //実行結果を確認
        if(isSuccess(result.toString())){

            //Klaxonを使ってJsonをパース
            val jsonObject = Klaxon().parseJsonObject(StringReader((result.get().content)))
            val resultText =  jsonObject["text"] as String

            //Azureが通らない特殊文字を_に置き換える
            return textCheck(resultText)
        }

        return null
    }

    private fun textCheck(sentence: String): String{
        //Azureが通らない特殊文字を_に置き換える

        var resultSentence = sentence
        """/,\,",',#,$,>,<,^,|""".split(",").forEach {resultSentence = resultSentence.replace(it, "_")}

        return resultSentence
    }
```

</div></details>

### COTOHA 固有表現抽出 API
COTOHA固有表現抽出APIは1日1000コールまで無料で利用することができます。1000という少ない数ですが毎日コツコツと不適切な文章を修正していけば、いつの日か**「きのこの山」を消滅に追い込み、より良い世界へ**と導くことができます。こちらのページから新規登録してください。([COTOHA for Developerss](https://api.ce-cotoha.com/contents/developers/index.html))

COTOHAではセキュリティ上の懸念から1時間に1回アクセストークンが変更となります。そのため、アクセストークンを1時間に1回問い合わせるようなロジックを組む必要があります。...が、今回は面倒くさいので修正のたびにアクセストークンを問い合わせることとします。

固有表現抽出APIのエンドポイントは`https://api.ce-cotoha.com/api/dev/nlp/v1/ne`です。
アクセストークンの問い合わせ先のエンドポイントは`https://api.ce-cotoha.com/v1/oauth/accesstokens`です。

<details><summary>以下コード（クリックして展開）</summary><div>

```Kotlin
    private data class UniqueWordData(val form: String, val _class: String, val beginIndex: Int, val endIndex: Int)

    private val cotohaRequestUrl = "https://api.ce-cotoha.com/api/dev/nlp/v1/ne"
    private val cotohaRequestAccessTokenUrl = "https://api.ce-cotoha.com/v1/oauth/accesstokens"
    private val cotohaClientID = "****************************"
    private val cotohaClientSecretID = "*************"
    
    private fun requestCotohaAccessToken(): String?{

        //COTOHAにAccessTokenをリクエストする
        val (_, _, result) = cotohaRequestAccessTokenUrl.httpPost()
                .header(mapOf("Content-Type" to "application/json"))
                .body(getCotohaRequestAccessTokenBody())
                .responseJson()

        //実行結果を確認
        if(isSuccess(result.toString())){

            //Klaxonを使ってJsonをパース
            val jsonObject = Klaxon().parseJsonObject(StringReader(result.get().content))
            val resultToken = jsonObject["access_token"] as String

            return resultToken
        }

        return null
    }

    private fun requestExtractionUniqueWord(text: String): Boolean {

        //COTOHAにAccessTokenをリクエストする
        val cotohaAccessToken = requestCotohaAccessToken() ?: return false

        //uniqueWordListを初期化
        uniqueWordList.clear()

        //COTOHA固有単語抽出APIにリクエストを投げる
        val (_, _, result) = cotohaRequestUrl.httpPost()
                .header(Pair("Content-Type", "application/json"), Pair("charset", "UTF-8"), Pair("Authorization", "Bearer $cotohaAccessToken"))
                .body(getCotohaRequestBody(text))
                .responseJson()

        //実行結果を確認
        if(isSuccess(result.toString())){

            //Klaxonを使ってJsonをパース
            val jsonObject = Klaxon().parseJsonObject(StringReader(result.get().content))
            val resultArray = jsonObject["result"] as JsonArray<*>

            for(resultObject in resultArray){
                if(resultObject is JsonObject){
                    uniqueWordList.add(
                         UniqueWordData(
                              resultObject["form"] as String,
                              resultObject["class"] as String,
                              resultObject["begin_pos"] as Int,
                              resultObject["end_pos"] as Int
                        )
                    )
                }
            }

            return true
        }

        return false
    }

    private fun getCotohaRequestAccessTokenBody() = """
        {
            "grantType": "client_credentials",
            "clientId": "$cotohaClientID",
            "clientSecret": "$cotohaClientSecretID"
        }
    """.trimIndent()

    private fun getCotohaRequestBody(sentence: String) = """
        {
            "sentence": "$sentence",
            "type": "kuzure"
        }
    """.trimIndent()
```

</div></details>

### Microsoft Azure Text Analytics API
Azureは世界でも有数の、高度な解析を可能とするAPI群です。今回は[Text Analytics API](https://docs.microsoft.com/ja-jp/azure/cognitive-services/text-analytics/how-tos/text-analytics-how-to-sentiment-analysis?tabs=version-2)を使用します。実はCOTOHAにも「感情推定API」というものがあったのですが、何度か使ってみたところ精度があまり良くなく、「きのこの山ってまずいよね。だよねだよね！」という**同意しかない文章**を、あろうことか「たけのこの里ってまずいよね。だよねだよね！」と修正してしまったのです。これはも**「たけのこの里」に対する涜神**以外の何物でもありません。私は憤怒しCOTOHAの開発元である**NTT DOCOMOに怒りの電話を入れようかと思いました**が、なんとか思いとどまりました。なので、今回はAzureを使用します。ちなみに、Azureで同じ文章を試してみたところ、正しく修正できました。**Microsoftは「たけのこ派」**であることが伺えます。

Azureアカウントを作成したら、Text Analyticsのダッシュボードを作成してください。その際に使用したリソース名がエンドポイントの一部となります。

<details><summary>以下コード（クリックして展開）</summary><div>

```Kotlin
    private val azureRequestUrl = "https://リソース名.cognitiveservices.azure.com/text/analytics/v2.1/sentiment?showStats=true"
    private val azureKey1 = "********************************"
    private val azureKey2 = "********************************"

    @SuppressLint("SetTextI18n")
    private fun requestTextAnalyze(){

        //Androidでは通信は別スレッド
        thread {
            //修正して差し上げるテキスト
            val targetText = FM_BeforeTextEdit.text.toString()

            //修正する単語
            val fromWord = FM_LeftBattlerEdit.text.toString()

            //修正後の単語
            val toWord = FM_RightBattlerEdit.text.toString()

            //COTOHA APIを使用して固有表現を抽出する
            val extractionResult = requestExtractionUniqueWord(targetText)

            //日本語で実行すると結果が曖昧なので、一度英語に翻訳してからAzureに渡す
            val translatedSentence = requestTranslate(targetText)

            //Azureに引き渡せる形になっているか確認
            if(extractionResult && translatedSentence != null) {

                //FuelでAPIリクエストを実行
                val (_, _, result) = azureRequestUrl.httpPost()
                    .header(Pair("Content-Type", "application/json"), Pair("Ocp-Apim-Subscription-Key", azureKey1))
                    .body(getAzureRequestBody(translatedSentence)).responseJson()

                //実行結果を確認
                if (isSuccess(result.toString())) {

                    //Klaxonを使用してJsonをパース
                    val jsonObject = Klaxon().parseJsonObject(StringReader(result.get().content))
                    val documentsArray = jsonObject["documents"] as JsonArray<*>

                    //どうせ1つしかリクエストしてないので、要素が一個以上あるかの確認と、型チェックを同時に実行
                    if (documentsArray.size >= 1 && documentsArray[0] is JsonObject) {
                        val score = (documentsArray[0] as JsonObject)["score"] as Double
                        val resultSentence = if (score > 0.5) {
                            //スコア0.5より上はポジティブ（肯定的）な内容
                            //ポジティブかつ「きのこの山」が含まれていたら修正

                            textExchanger(targetText, fromWord, toWord)
                        } else {
                            //スコア0.5以下はネガティブ（否定的）な内容
                            //ネガティブかつ「たけのこの里」が含まれていたら修正

                            textExchanger(targetText, toWord, fromWord)
                        }

                        return@thread
                    }
                }
            }
        }
    }

```

</div></details>

### レイアウト
画面レイアウトを作成します。この辺は正直好みです。普段から**「たけのこの里」を選ぶようなセンスの良い皆様**であればちょちょいのちょいだと思います。今回はFragmentとして作成しますがActivityでもなんでも構いません。MaterialComponentsを使用しているため、gradleに以下のように記述します。

<details><summary>以下コード（クリックして展開）</summary><div>

```Gradle
dependencies {
    implementation 'com.google.android.material:material:1.1.0' //追加

    implementation 'com.beust:klaxon:5.0.1'

    implementation 'com.github.kittinunf.fuel:fuel:1.12.0'
    implementation 'com.github.kittinunf.fuel:fuel-android:1.12.0'
}
```

</div></details>

シンプルなつくりでもMaterialComponentsを使えば、案外華やかな見た目になります。今回はTextInputLayoutとTextInputEditを使用します。Googleログインの画面などで表示されるあのレイアウトです。こちらのレイアウトの詳しい使い方については[【TextInputLayout/TextInputEdit】Material.TextFieldのカスタマイズチートシート](https://qiita.com/CaptainPag/items/5d0b7500e4537b04c4e6#outlined-text-field-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA)の記事をご覧ください。

<details><summary>以下コード（クリックして展開）</summary><div>

```XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorBackground">

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/FM_LeftBattlerField"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        app:counterEnabled="true"
        app:counterMaxLength="10"
        app:layout_constraintEnd_toStartOf="@+id/FM_VStext"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/FM_LeftBattlerEdit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/BeforeWord"
            android:text="@string/Kinoko" />

    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/FM_RightBattlerField"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="24dp"
        app:counterEnabled="true"
        app:counterMaxLength="10"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/FM_VStext"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/FM_RightBattlerEdit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/AfterWord"
            android:text="@string/Takenoko" />

    </com.google.android.material.textfield.TextInputLayout>

    <TextView
        android:id="@+id/FM_VStext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="⇒"
        android:textColor="@color/colorChar"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="@+id/FM_RightBattlerField"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/FM_RightBattlerField" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:layout_marginBottom="16dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toTopOf="@+id/FM_FixButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/FM_LeftBattlerField">

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/FM_BeforeTextField"
            style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginStart="24dp"
            android:layout_marginEnd="24dp"
            android:layout_weight="1"
            app:counterEnabled="true"
            app:counterMaxLength="60"
            app:endIconMode="clear_text"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/FM_LeftBattlerField">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/FM_BeforeTextEdit"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:hint="@string/BeforeText" />

        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/FM_AfterTextField"
            style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginStart="24dp"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="24dp"
            android:layout_weight="1"
            app:counterEnabled="true"
            app:counterMaxLength="60"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/FM_BeforeTextField">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/FM_AfterTextEdit"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:hint="@string/AfterText"
                android:inputType="none" />

        </com.google.android.material.textfield.TextInputLayout>

        <ScrollView
            android:id="@+id/FM_ScrollView"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginTop="16dp"
            android:layout_weight="1">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <TextView
                    android:id="@+id/FM_Log"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="24dp"
                    android:layout_marginEnd="24dp"
                    android:text="@string/AnalyzeResult"
                    android:textColor="@color/colorCharSec" />

            </LinearLayout>
        </ScrollView>
    </LinearLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/FM_FixButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="@string/FixForTheWorld"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

string.xmlも掲載しておきます

```XML
<resources>
    <string name="app_name">Takenoko</string>

    <string name="Setting">設定</string>
    <string name="Help">ヘルプ</string>

    <string name="battle">修正</string>
    <string name="battleResult">修正の履歴</string>

    <string name="Takenoko">たけのこの里</string>
    <string name="Kinoko">きのこの山</string>

    <string name="FixForTheWorld">社会をより良くするために修正を実行</string>
    <string name="AfterWord">修正後の単語</string>
    <string name="BeforeWord">修正する単語</string>
    <string name="AfterText">修正済みの文章</string>
    <string name="BeforeText">修正して差し上げる文章</string>
    <string name="AnalyzeResult">解析結果をここに表示します</string>

    <string name="processing">処理中です</string>
    <string name="error">エラーが発生しました</string>

    <string name="drawer_open">Navigation drawer open</string>
    <string name="drawer_close">Navigation drawer close</string>

    <string name="textEmptyError">必須項目です</string>
    <string name="textLengthError">文字数オーバーです</string>
</resources>
```

</div></details>

### プログラム全文
Fragmentの部分だけですが、全文を公開しておきます。

<details><summary>以下コード（クリックして展開）</summary><div>

```Kotlin
package caios.android.takenoko

import android.annotation.SuppressLint
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.beust.klaxon.JsonArray
import com.beust.klaxon.JsonObject
import com.beust.klaxon.Klaxon
import com.github.kittinunf.fuel.android.extension.responseJson
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.fuel.httpPost
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import kotlinx.android.synthetic.main.fragment_main.*
import java.io.StringReader
import kotlin.concurrent.thread

class MainFragment : Fragment(){

    private data class UniqueWordData(val form: String, val _class: String, val beginIndex: Int, val endIndex: Int)

    private val uniqueWordList = mutableListOf<UniqueWordData>()

    private val googleTranslateUrl = "作成したAPIのエンドポイント"

    private val azureRequestUrl = "https://リソース名.cognitiveservices.azure.com/text/analytics/v2.1/sentiment?showStats=true"
    private val azureKey1 = "********************************"
    private val azureKey2 = "********************************"

    private val cotohaRequestUrl = "https://api.ce-cotoha.com/api/dev/nlp/v1/ne"
    private val cotohaRequestAccessTokenUrl = "https://api.ce-cotoha.com/v1/oauth/accesstokens"
    private val cotohaClientID = "*********************************"
    private val cotohaClientSecretID = "*************"

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        FM_LeftBattlerEdit.addTextChangedListener(getTextWatcher(FM_LeftBattlerField, 10))
        FM_RightBattlerEdit.addTextChangedListener(getTextWatcher(FM_RightBattlerField, 10))
        FM_BeforeTextEdit.addTextChangedListener(getTextWatcher(FM_BeforeTextField, 60))

        FM_AfterTextEdit.keyListener = null
        
        FM_FixButton.setOnClickListener {
            it.isEnabled = false
            FM_AfterTextEdit.text?.clear()
            FM_Log.text = ""
            requestTextAnalyze()
        }
    }

    @SuppressLint("SetTextI18n")
    private fun requestTextAnalyze(){
        Log.d(TAG, "requestTextAnalyze")

        //Androidでは通信は別スレッド
        thread {
            //修正して差し上げるテキスト
            val targetText = FM_BeforeTextEdit.text.toString()

            //修正する単語
            val fromWord = FM_LeftBattlerEdit.text.toString()

            //修正後の単語
            val toWord = FM_RightBattlerEdit.text.toString()

            //COTOHA APIを使用して固有表現を抽出する
            val extractionResult = requestExtractionUniqueWord(targetText)

            //日本語で実行すると結果が曖昧なので、一度英語に翻訳してからAzureに渡す
            val translatedSentence = requestTranslate(targetText)

            //Azureに引き渡せる形になっているか確認
            if(extractionResult && translatedSentence != null) {

                //FuelでAPIリクエストを実行
                val (_, _, result) = azureRequestUrl.httpPost()
                    .header(Pair("Content-Type", "application/json"), Pair("Ocp-Apim-Subscription-Key", azureKey1))
                    .body(getAzureRequestBody(translatedSentence)).responseJson()

                //実行結果を確認
                if (isSuccess(result.toString())) {

                    //Klaxonを使用してJsonをパース
                    val jsonObject = Klaxon().parseJsonObject(StringReader(result.get().content))
                    val documentsArray = jsonObject["documents"] as JsonArray<*>

                    //どうせ1つしかリクエストしてないので、要素が一個以上あるかの確認と、型チェックを同時に実行
                    if (documentsArray.size >= 1 && documentsArray[0] is JsonObject) {
                        val score = (documentsArray[0] as JsonObject)["score"] as Double
                        val resultSentence = if (score > 0.5) {
                            //スコア0.5より上はポジティブ（肯定的）な内容
                            //ポジティブかつ「きのこの山」が含まれていたら修正

                            log("Result -> score positive $score")

                            textExchanger(targetText, fromWord, toWord)
                        } else {
                            //スコア0.5以下はネガティブ（否定的）な内容
                            //ネガティブかつ「たけのこの里」が含まれていたら修正

                            log("Result -> score negative $score")

                            textExchanger(targetText, toWord, fromWord)
                        }

                        log("[SUCCESS]: $resultSentence")

                        FM_AfterTextEdit.post {
                            FM_FixButton.isEnabled = true
                            FM_AfterTextEdit.setText(resultSentence)
                        }

                        return@thread
                    }
                    else log("[FAILED 1]: ${documentsArray.size}")
                }
                else log("[FAILED 2]: $result")
            }
            else log("[FAILED 3]: $extractionResult, ${translatedSentence != null}")

            FM_AfterTextEdit.post {
                Toast.makeText(requireContext(), getString(R.string.error), Toast.LENGTH_SHORT).show()
                FM_FixButton.isEnabled = true
            }
        }
    }

    private fun requestCotohaAccessToken(): String?{
        log("requestCotohaAccessToken")

        //COTOHAにAccessTokenをリクエストする
        val (_, _, result) = cotohaRequestAccessTokenUrl.httpPost()
                .header(mapOf("Content-Type" to "application/json"))
                .body(getCotohaRequestAccessTokenBody())
                .responseJson()

        //実行結果を確認
        if(isSuccess(result.toString())){

            //Klaxonを使ってJsonをパース
            val jsonObject = Klaxon().parseJsonObject(StringReader(result.get().content))
            val resultToken = jsonObject["access_token"] as String

            log("Result -> ${resultToken.length} length")

            return resultToken
        }

        log("API request failed. -> $result")

        return null
    }

    private fun requestExtractionUniqueWord(text: String): Boolean {

        //COTOHAにAccessTokenをリクエストする
        val cotohaAccessToken = requestCotohaAccessToken() ?: return false

        log("requestExtractionUniqueWord ($text)")

        //uniqueWordListを初期化
        uniqueWordList.clear()

        //COTOHA固有単語抽出APIにリクエストを投げる
        val (_, _, result) = cotohaRequestUrl.httpPost()
                .header(Pair("Content-Type", "application/json"), Pair("charset", "UTF-8"), Pair("Authorization", "Bearer $cotohaAccessToken"))
                .body(getCotohaRequestBody(text))
                .responseJson()

        //実行結果を確認
        if(isSuccess(result.toString())){

            //Klaxonを使ってJsonをパース
            val jsonObject = Klaxon().parseJsonObject(StringReader(result.get().content))
            val resultArray = jsonObject["result"] as JsonArray<*>

            for(resultObject in resultArray){
                if(resultObject is JsonObject){
                    uniqueWordList.add(
                            UniqueWordData(
                                    resultObject["form"] as String,
                                    resultObject["class"] as String,
                                    resultObject["begin_pos"] as Int,
                                    resultObject["end_pos"] as Int
                            )
                    )
                }
            }

            log("Result -> Unique word list size :${uniqueWordList.size}")

            return true
        }

        log("API request failed. -> $result")

        return false
    }

    private fun requestTranslate(text: String): String?{
        log("requestTranslate ($text)")

        //自作したGoogle翻訳APIにリクエストを投げる
        val (_, _, result) = "$googleTranslateUrl?text=$text&source=ja&target=en".httpGet().responseJson()

        //実行結果を確認
        if(isSuccess(result.toString())){

            //Klaxonを使ってJsonをパース
            val jsonObject = Klaxon().parseJsonObject(StringReader((result.get().content)))
            val resultText =  jsonObject["text"] as String

            log("API Result -> $resultText")

            //Azureが通らない特殊文字を置き換える
            return textCheck(resultText)
        }

        log("API request failed. -> $$result")

        return null
    }

    private fun textExchanger(sentence: String, fromText: String, toText: String): String{
        //sentenceに含まれているfromTextをtoTextに変換して返す

        var resultSentence = sentence
        uniqueWordList.forEach {
            if(it.form == fromText) resultSentence = resultSentence.replaceRange(it.beginIndex, it.endIndex, toText)
        }

        return resultSentence
    }

    private fun textCheck(sentence: String): String{
        //Azureが通らない特殊文字を_に置き換える

        var resultSentence = sentence
        """/,\,",',#,$,>,<,^,|""".split(",").forEach {resultSentence = resultSentence.replace(it, "_")}

        log("textCheck -> $resultSentence")

        return resultSentence
    }

    private fun isSuccess(result: String): Boolean = (result.indexOf("Success") != -1)

    @SuppressLint("SetTextI18n")
    private fun log(message: String){
        Log.d(TAG, message)

        FM_Log.post {
            FM_Log.text = "${FM_Log.text}\n\n$message"
            FM_ScrollView.fullScroll(View.FOCUS_DOWN)
        }
    }

    private fun getAzureRequestBody(sentence: String) = """
            {
                "documents": [
                    {
                        "language": "en",
                        "id": "1",
                        "text": "$sentence"
                    }
                ]
            }
        """.trimIndent()

    private fun getCotohaRequestAccessTokenBody() = """
        {
            "grantType": "client_credentials",
            "clientId": "$cotohaClientID",
            "clientSecret": "$cotohaClientSecretID"
        }
    """.trimIndent()

    private fun getCotohaRequestBody(sentence: String) = """
        {
            "sentence": "$sentence",
            "type": "kuzure"
        }
    """.trimIndent()

    private fun getTextWatcher(editor: TextInputLayout, size: Int) = object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {
                when {
                    s?.toString()?.isEmpty() == true -> editor.error = getString(R.string.textEmptyError)
                    s?.length ?: 0 > size         -> editor.error = getString(R.string.textLengthError)
                    else                        -> editor.error = null
                }
            }

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
        }

    companion object{
        const val TAG = "<TAKENOKO>"
    }
}
```

</div></details>

## 修正時のスクショいろいろ
![5119.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420833/4cea6fb1-1c85-8e44-a0ac-d9a00b41b093.jpeg)
![5120.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420833/ef1e7bcf-5384-45c5-8886-f22af6cc0276.jpeg)
![5121.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420833/dea77db1-cd47-c5b8-5f6a-12fbf5749565.jpeg)
![5122.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420833/dd17be28-c050-d69b-2ef6-d2bc7700a1c9.jpeg)

## 修正例いろいろ
```
きのこの山はおいしい。神の食べ物か。
修正済みテキスト：たけのこの里はおいしい。神の食べ物か。
```
```
チョコと土台のハーモニーが素晴らしい。たけのこの里は他のお菓子の追随を許さない。☆5です。
修正済みテキスト：チョコと土台のハーモニーが素晴らしい。たけのこの里は他のお菓子の追随を許さない。☆5です。
```
```
きのこの山まずい。
修正済みテキスト：きのこの山まずい。
```
```
たけのこの里大好きです
修正済みテキスト：たけのこの里大好きです
```
```
きのこの山最高！ チョコを先食べてクッキーだけにして食べたら最高！
修正済みテキスト：たけのこの里最高！ チョコを先食べてクッキーだけにして食べたら最高！
```
```
おれ今たけのこの里食ってるけどうまいｗ
修正済みテキスト：おれ今たけのこの里食ってるけどうまいｗ
```
```
手を汚さず食べれるから きのこの山かな
修正済みテキスト：手を汚さず食べれるから たけのこの里かな
```
```
きのこの山は安っぽいからいらない。
修正済みテキスト：きのこの山は安っぽいからいらない。
```
```
すぎのこ村
修正済みテキスト：帰れ
```

## まとめ
いかがだったでしょうか。無事、正しい文章に修正して差し上げることができました。このアプリは近く公開も予定していますので、皆様ぜひダウンロードしてより**良い世界をともに築いていきましょう**。私としては、キーボードアプリにこのプログラムを仕込んで、邪悪な文章だと判定したら**確定する前に修正して差し上げる**アプリの開発を考えております。こちらも公開にかぎつけた暁には皆さまダウンロードよろしくお願いいたします。

今日も「きのこたけのこ戦争」が様々な場所で勃発しておりますが、一度冷静になってこの記事に目を通してみてください。**正しい歴史認識と誤りを矯正するアプリを入手する**ことができますよ。この記事が、1つでも多くの争いを止めることを祈っております。



......おいちょっと待て「きのこたけのこ戦争」ってなんで「きのこ」が先なんだよ。普通だったら「たけのこ」が先で、**「たけのこきのこ戦争」**だろ...

[^1]: [たけのこ党に吉田沙保里！　きのこ派が戦意喪失「勝ち目ない」「終戦記念日」](https://news.nicovideo.jp/watch/nw3328166)
[^2]: [きのこの山とたけのこの里、どちらが優れているの？薬剤師に聞いてみた。] (https://liginc.co.jp/267503)
[^3]: [これは外国人にウケる!!　と思う日本のお菓子1位「たけのこの里」](https://news.livedoor.com/article/detail/10961935/)