アドベントカレンダー Android 初心者 15日目の記事です。 まだ枠が空いているので記事を見た人は登録するか3人の Android エンジニアに送りつけてください。
初心者ということで試してみた記事を書いてみたいと思います。
Android 8 ~ テキスト分類API が追加されました。
TextClassifier | Android Developers
Android 9 からは OEM が独自に拡張できたりとだいぶ力を入れているようです。
Implementing Text Classification | Android Open Source Project
このAPIの弱点は API 26 (Android 8 ) からしか使えないという点です。
(機械学習的なの使われてるらしいので使えるとかっこよさそう(小並感
テキスト分類APIはAndroid のスマートテキスト機能を実装するために使われている
Android には、住所、電話番号、メールアドレスなどのメールアプリで見るとリンクされてそうなのを色々なアプリの上で、テキストが選択するだけで色々なアクションを簡単に実行する機能があります。 Google はそれにスマートテキストって名前を使ってます。
以下の画像は、下記リンク先の画像の引用です。
テキスト選択されている住所っぽい文字列に対して地図で表示したら便利じゃね?ってことで Map で開くアクションが自動的に表示されてますね。
ソースコードみてないのでよく知らないのですが、中では選択されたテキストに対してこのテキスト分類APIを利用して分類、さらに Action (中身はIntent)を取得することができるのでそれを menu に追加して表示してる感じです。ぱっと見は難しそうですが、以外に単純。
とはいえそこまで汎用的に使えれるものではない
テキスト分類、普通にアプリに組み込むとしたら機能不足なのでそのまま使えれるシチュエーションは少ないかと思います。
このAPIの目的自体がスマートテキストを実現するものですし...
とはいえ、試していきます。
Android のバージョン問わず同じ記法でテキスト分類が使えるようになった
API 26 以前でテキスト分類するには自前でやるかなと思います?(そこが知識不足でよくわかりません...
仮にこのパッケージが出ていない状態で API 26 以前に対応するとしたら以下のような選択になると思います。
これが、このパッケージが出ていることでパッケージの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); }
既存のAPIとの差異
既存で実装されているAPIからはちょっと差異があります。
そのことは AndroidX のリリースノートにも載っています。
AndroidX release notes | Android Developers
表現がリファクタリングとなっていますし、今後さらに変更が加えられてくることが想像できるので使う場合はクラスでラップして変更に強い形にしたほうがいいかと思います。
TextClassificationManager の取得方法が変更されている
既存の実装では、 Context#getSystemService() から取得する方式でした。 しかし、 AndroidX 版では 他のクラスと同じように取得するように変更されています。
使わなさそうなメソッドが削除されている
TextClassification#getEntity() などのメソッドが削除されています。
サンプル
サンプルとしてこのAPI を利用してる部分を切り出してみました。
Action が使える場合は無条件で Intent を送るようにしてます。
画面とか
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 のソースコードが入ってるのは知らなかったです。
- モンスター飲みたい