くうと徒然なるままに

モバイルアプリを作りながらバックエンドも作っています。

AndroidX の androidx.textclassifier を試してみる

アドベントカレンダー Android 初心者 15日目の記事です。 まだ枠が空いているので記事を見た人は登録するか3人の Android エンジニアに送りつけてください。

qiita.com

初心者ということで試してみた記事を書いてみたいと思います。

Android 8 ~ テキスト分類API が追加されました。

TextClassifier  |  Android Developers

Android 9 からは OEM が独自に拡張できたりとだいぶ力を入れているようです。

Implementing Text Classification  |  Android Open Source Project

このAPIの弱点は API 26 (Android 8 ) からしか使えないという点です。

機械学習的なの使われてるらしいので使えるとかっこよさそう(小並感

テキスト分類APIAndroid のスマートテキスト機能を実装するために使われている

Android には、住所、電話番号、メールアドレスなどのメールアプリで見るとリンクされてそうなのを色々なアプリの上で、テキストが選択するだけで色々なアクションを簡単に実行する機能があります。 Google はそれにスマートテキストって名前を使ってます。

以下の画像は、下記リンク先の画像の引用です。 

テキスト選択されている住所っぽい文字列に対して地図で表示したら便利じゃね?ってことで Map で開くアクションが自動的に表示されてますね。

ソースコードみてないのでよく知らないのですが、中では選択されたテキストに対してこのテキスト分類APIを利用して分類、さらに Action (中身はIntent)を取得することができるのでそれを menu に追加して表示してる感じです。ぱっと見は難しそうですが、以外に単純。

f:id:kuxumarin:20181210182322p:plain

news.mynavi.jp

とはいえそこまで汎用的に使えれるものではない

テキスト分類、普通にアプリに組み込むとしたら機能不足なのでそのまま使えれるシチュエーションは少ないかと思います。

このAPIの目的自体がスマートテキストを実現するものですし...

とはいえ、試していきます。

Android のバージョン問わず同じ記法でテキスト分類が使えるようになった

API 26 以前でテキスト分類するには自前でやるかなと思います?(そこが知識不足でよくわかりません...

仮にこのパッケージが出ていない状態で API 26 以前に対応するとしたら以下のような選択になると思います。

  • 全てのバージョンで自前でテキスト分類を動かしてく
  • API 26 以下は自前実装、それ以上は標準のAPIを利用してく

これが、このパッケージが出ていることでパッケージのAPIがOS のバージョンごとの差異を吸収してくれます。

実際に中のソースを読んで行くと OSのバージョンによって処理をプラットフォームに実装されたもの or legacy TextClassication かで呼び分けてます。

どうでもいいんですけど、 Legacy の方のコードって微妙に汚い...(Google のエンジニアの方が作ったのであまり批判できないけども...

130行目を参照 

    private static TextClassifier defaultTextClassifier(@NonNull Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            return PlatformTextClassifierWrapper.create(context);
        }
        return LegacyTextClassifier.of(context);
    }

textclassifier/src/main/java/androidx/textclassifier/TextClassificationManager.java - platform/frameworks/support - Git at Google

既存のAPIとの差異

既存で実装されているAPIからはちょっと差異があります。

そのことは AndroidX のリリースノートにも載っています。

AndroidX release notes  |  Android Developers

表現がリファクタリングとなっていますし、今後さらに変更が加えられてくることが想像できるので使う場合はクラスでラップして変更に強い形にしたほうがいいかと思います。

TextClassificationManager の取得方法が変更されている

既存の実装では、 Context#getSystemService() から取得する方式でした。 しかし、 AndroidX 版では 他のクラスと同じように取得するように変更されています。

使わなさそうなメソッドが削除されている

TextClassification#getEntity() などのメソッドが削除されています。

サンプル

サンプルとしてこのAPI を利用してる部分を切り出してみました。

Action が使える場合は無条件で Intent を送るようにしてます。

画面とか

f:id:kuxumarin:20181210190016p:plain

f:id:kuxumarin:20181210190029p:plain

f:id:kuxumarin:20181210190041p:plain

f:id:kuxumarin:20181210190055p:plain

MainActivity.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/source_edit_text"
            android:layout_width="0dp"
            android:layout_height="wrap_content" 
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp" 
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp" 
            android:layout_marginTop="8dp" 
            app:layout_constraintTop_toTopOf="parent"/>

    <TextView
            android:id="@+id/result_text_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:text="Hello World!"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/analyze_material_button"
            android:layout_marginBottom="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp"/>

    <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/analyze_material_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/source_edit_text"
            android:text="解析する"
            app:layout_constraintStart_toStartOf="@+id/source_edit_text"
            app:layout_constraintEnd_toEndOf="@+id/source_edit_text"
            android:layout_marginEnd="8dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

package kuxu.textclassfier

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.textclassifier.TextClassification
import androidx.textclassifier.TextClassificationManager
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {


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

        analyze_material_button.setOnClickListener {
            val tcm = TextClassificationManager.of(this)
            GlobalScope.launch {
                val sourceText = source_edit_text.text?.toString() ?: ""
                val textClassifier = tcm.textClassifier
                val request = TextClassification.Request.Builder(
                    sourceText,
                    0,
                    sourceText.length
                ).build()
                val result = textClassifier.classifyText(request)
                var resultText = ""

                resultText += (0..result.entityTypeCount - 1)
                    .map { "${result.getEntityType(it)}:${result.getConfidenceScore(result.getEntityType(it))}%\n" }
                    .fold("") { x, y -> x + y }

                resultText += result.actions
                    .map { "${it.contentDescription.toString()}:${it.isEnabled}\n" }
                    .fold("") { x, y -> x + y }

                result_text_view.text = resultText

                if (!result.actions.isEmpty()) {
                    result.actions.first().actionIntent.send()
                }
            }
        }
    }
}

app/build.gradle

implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.textclassifier:textclassifier:1.0.0-alpha01'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1'

まとめ

  • ドキュメント少なくてちょっと辛かった
  • AOSP に androidx のソースコードが入ってるのは知らなかったです。
  • モンスター飲みたい