- 投稿日:2020-12-16T23:22:37+09:00
Flutterで辞書APIを使ってみた
はじめに
今までAndroidのアプリをJavaで作っていたんですが、新しいことをやりたくなったのでFlutterでアプリを作ってみました。
デモアプリを真似するだけでは面白くないので、Oxford Dictionaries APIを用いて簡単な辞書機能を搭載したアプリにしました。
Flutter
はレイアウトも、dart
で書かないといけない(これがいいところなのかもしれない)ので、なかなか思ったように表示されなくて難しかったです。Oxford Dictionaries API
今回使用した、英英辞書はOxford Dictionaries APIです。
https://developer.oxforddictionaries.com/このAPIの特徴はプロトタイプ版なら月1,000リクエストまで無料で使えるところです。
辞書APIの使い方
App IDとApp Keyを取得
入力したメールアドレスに認証用リンクが届くのでそこにアクセス
(Gmailによってプロモーションに分類されていて、このメールを見つけるのに20分くらいかかった)サイトにもどってサインインする
サインインすると、
GET YOUR API KEY
と書いてあった部分が、CREDENTIALS
になっているので、そこを選択。
プログラム部分
アプリでHTTP通信をできるようにします。
まずは、pubspec.yaml
に以下を追記します。pubspec.yamldependencies: flutter: sdk: flutter http: ^0.12.2 # 追記Androidの場合は
AndroidManifest.xml
に以下を追記します。AndroidManifest.xml<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.github.labsprout.english_cards"> <!-- 以下の一行を追加 --> <uses-permission android:name="android.permission.INTERNET"/>メインの部分はこんな感じになります。
(ちなみに、Python
やJava
用のプログラムはOxford Dictionaries APIのページにサンプルがあります)OxfordDictionary.dartimport 'package:http/http.dart' as http; Future<String> getRequest(String head) async { head = head.toLowerCase(); final _language = "en-us"; // 1 final _appId = "<your app id>"; // 2 final _appKey = "<your app key>"; // 3 final _link = "https://od-api.oxforddictionaries.com:443/api/v2/entries/" + _language + "/" + head + "?fields=definitions%2Cexamples%2Cpronunciations&strictMatch=false"; // 4 try { final response = await http.get(_link, // 5 headers: { "Accept": "application/json", "app_id": _appId, "app_key": _appKey }); return response.body.toString(); } catch (e, stackTrace) { return e + " " + stackTrace; // 6 } }関数名のあとの
async
は非同期処理という意味です。それに伴って、返り値はString
ではなくFuture<String>
になってます。
(将来String
を返すよって意味だと思います)
- 言語を選択(
en-us
はアメリカ英語という意味)- 自分の
AppID
に変更- 自分の
AppKey
に変更- 通信先のURL(
fiels
はどんな情報が必要かを指定する部分です。指定しなければ全部の情報を得られます)_link
で指定したURLと通信(今回通信するためには、app_id
とapp_key
を指定する必要があるので、headers
で指定しています)- エラーだったときは、
stackTrace
を返すメインのプログラムからこの関数を呼び出します。
main.dartFutureBuilder( // 1 future: getRequest(head), builder: (BuildContext context, AsyncSnapshot<String> snapshot) { // 2 } )
getRequest(head)
で得られる値はFuture<String>
なので、FutureBuilder
を使って処理- ここで受け取った値を処理
snapshot.data // 関数から得られた値 snapshot.connectionState // 接続状況得られる値は
json
なので扱いやすいように(?)独自のクラスに変換しました。変換先のクラスはこんな感じにしました。
WordData.dartclass Word { String head; String json; Word(this.head, this.json); } class WordData { String head; List<Entry> entries; WordData(this.head, this.entries); } class Entry { String audioURL; String ipa; List<Sense> senses; String category; Entry(this.audioURL, this.ipa, this.senses, this.category); } class Sense { String def; String example; Sense(this.def, this.example); }
json
から独自クラスへの変換をする関数はこんな感じ。WordData.dartimport 'dart:convert'; WordData convertToWordData(Word word) { List<Entry> entries = []; Map map = jsonDecode(word.json); List jsonEntries = map["results"][0]["lexicalEntries"]; jsonEntries.forEach((element) { String audioFile = element["entries"][0]["pronunciations"][1]["audioFile"]; String ipa = element["entries"][0]["pronunciations"][1]["phoneticSpelling"]; List jsonSenses = element["entries"][0]["senses"]; List<Sense> senses = []; jsonSenses.forEach((element1) { String def = element1["definitions"][0]; print(def); print(element1); String example = ""; if (element1.containsKey("examples")) { print("yes"); example = element1["examples"][0]["text"]; } print("all"); Sense sense = new Sense(def, example); senses.add(sense); }); String category = element["lexicalCategory"]["text"]; Entry entry = new Entry(audioFile, ipa, senses, category); entries.add(entry); }); return new WordData(word.head, entries); }
jsonDecode()
は自動でmap
だったりlist
だったりに変換してくれるので扱いやすかったです。あとは、画面に表示する
widget
を作ります。main.dartWord word = new Word(head, snapshot.data); WordData wordData = convertToWordData(word); // 1 List<Widget> widgets = []; // 2 wordData.entries.forEach((entry) { // 3 widgets.add(new Divider()); widgets.add(new Text( head + ' (' + entry.category + ') [' + entry.ipa + ']', style: TextStyle( fontSize: 18.0, color: Colors.blue ), )); entry.senses.forEach((sense) { widgets.add(new Text( sense.def, style: TextStyle( fontSize: 16.0 ) )); if (sense.example != "") { widgets.add(new Text( ' ex.) ' + sense.example, style: TextStyle( fontSize: 16.0, color: Colors.grey[700] ), )); } }); }); // 省略 return Column( // 4 children: widgets, crossAxisAlignment: CrossAxisAlignment.start, );
- 独自のクラスに変換
- 画面に表示する
widget
をリスト(widgets
)にして保管wordData
に入っている情報をforEach
で取り出すwidgets
に入っているwidget
をすべて縦にならべるファイルの読み書き
ファイルの読み書きは、
Java
に比べて簡単にできました。まず、アプリ内のパスを取得できるようにするために
pubspec.yaml
に以下を追記してpathpath_provider
を読み込みます。pubspec.yamldependencies: flutter: sdk: flutter path_provider: any # 追記保存
saveWordText(String word, String json) async { final directory = await getApplicationDocumentsDirectory(); // 1 final file = File(directory.path + '/word_' + word + '.txt'); // 2 file.writeAsString(json); // 3 }
- ファイルを保存できるディレクトリを取得
- 適当なパスを指定してファイルを取得
- 書き込み処理
読み込み
Future<String> getWordText(String word) async { final directory = await getApplicationDocumentsDirectory(); File file = new File(directory.path + '/word_' + word + '.txt'); if (await file.exists()) { return file.readAsString(); // (*) } else { return ""; } }ファイルが存在するかチェックした後、(*)の部分で読み込みを行ってます。
削除
deleteWordText(String word) async { final directory = await getApplicationDocumentsDirectory(); File file = new File(directory.path + '/word_' + word + '.txt'); if (file.existsSync()) { file.delete(); } }おわりに
プログラム全体はGithubに上げてあります。
https://github.com/LabSprout/english_cards記事書いてて気がついたんですけど、型推論使ってたり使ってなかたっりぐちゃぐちゃですね。ごめんなさい。
(これって使い分けあったりするんだろうか)Flutterで自分のアプリを作るのも、Qiitaに記事を投稿するのも初めてなので微妙なところがあったら、ぜひ教えて下さい。
最後まで読んで下さりありがとうございます。
- 投稿日:2020-12-16T20:55:37+09:00
【Android】LinearLayoutの使い方
プログラミング勉強日記
2020年12月16日
LinearLayoutを始めて使ったので、LinearLayoutとは何か・使い方を簡単にまとめる。LinearLayoutとは
LinearLayout(読み方:「リニアレイアウト」)は、単純なレイアウトの1つで、子要素を縦・横の一列に並べるレイアウトである。
android:orientation
属性にvertical
,horizontal
を指定することでそれぞれ縦、横に並べることができる。LinearLayoutを使って子要素を縦一列に並べる方法
app/src/main/res/layout/activity_main.xmlでレイアウトエディタのデザインから配置することができる。
- パレット内にあるLayoutを選択
- LinearLayout(Vertical)を選択
- プレビュー画面にドラッグして配置する
4. 同様にして配置したいレイアウトをプレビュー画面にドラッグする
※この時点ではLinearLayoutにConstraintLayoutの制約がないためエラーが出ている。LinearLayoutに
tools:ignore="MissingConstraints"
を記述することでエラーを消した。(ConstraintLayoutの制約については昨日の記事で挙げている)
activity_main.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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="409dp" android:layout_height="729dp" android:orientation="vertical" tools:layout_editor_absoluteX="1dp" tools:layout_editor_absoluteY="1dp" tools:ignore="MissingConstraints"> <Button android:id="@+id/button4" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button5" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button" /> <Button android:id="@+id/button6" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>要素を横に並べたい場合は、LinearLayout(Horizontal)にする。やることは縦に並べるときと同じ。
参考文献
縦または横一列に要素を並べるリニアレイアウト (LinearLayout)
Androidアプリ開発のLinearLayoutの使い方【初心者向け】
- 投稿日:2020-12-16T17:02:05+09:00
GitHub Actions で Android プロジェクトのプルリクのチェックを行う(ゆめみ社の事例)
これは ゆめみ Advent Calendar 2020 の9日目の記事です。今回は、ゆめみの Android プロジェクトでよく導入している、GitHub Actions でのプルリクチェックのワークフローを紹介したいと思います
ゆめみの Android グループは、約20以上の多種多様なお客様の Android プロジェクトに携わっています。一部例外はありますが、多くのプロジェクトの CI は、Bitrise と GitHub Actions を併用して構築しています。以前はプルリクのチェックは Bitrise を利用して実施していましたが、最近は GitHub Actions に乗り換えつつあります。
プルリクのチェックでは ktlint、Android Lint、Local unit test を実行し、Danger を利用してチェック結果をコメントさせています。これを実現するワークフローは次の通りです。(この記事の為にコメントを多めにいれています。)
.github/workflows/check-pull-request.ymlname: Check pull request on: pull_request env: # 実行する Gradle コマンド(プロジェクトによって調整してください。) GRADLE_KTLINT_TASK: 'ktlint' GRADLE_ANDROID_LINT_TASK: 'lintDevelopDebug' GRADLE_UNIT_TEST_TASK: 'testDevelopDebugUnitTest' jobs: check: name: Check pull request runs-on: ubuntu-18.04 # Java や Ruby を利用するので念の為、環境を固定 steps: - name: Check out uses: actions/checkout@v2 with: fetch-depth: 0 # 0 を指定しないと Danger でエラーとなる。代わりに ref: ${{ github.event.pull_request.head.sha }} としてもいいかもしれません - name: Set up JDK uses: actions/setup-java@v1 with: java-version: 1.8 - name: Restore gradle cache # Gradle のキャッシュをリストア uses: actions/cache@v2 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle', '**/*.gradle.kts') }} - name: Set up Ruby # gem を利用するので Ruby をセットアップ uses: actions/setup-ruby@v1 with: ruby-version: '2.6' - name: Get gem info env: # Danger で利用する gem をここで列挙 PACKAGES: danger:6.2.0 danger-checkstyle_format:0.1.1 danger-android_lint:0.0.8 danger-junit:1.0.0 id: gem-info run: | echo "::set-output name=dir::$(gem environment gemdir)" # キャッシュするgemのディレクトリ echo "::set-output name=packages::$PACKAGES" # install 用の文字列 echo "::set-output name=key::$(echo $PACKAGES | tr ' ' '-')" # キャッシュのキー文字列 - name: Restore gem cache # gem のキャッシュをリストア uses: actions/cache@v2 with: path: ${{ steps.gem-info.outputs.dir }} key: ${{ runner.os }}-gem-${{ steps.gem-info.outputs.key }} - name: Run ktlint run: ./gradlew $GRADLE_KTLINT_TASK - name: Run Android Lint run: ./gradlew $GRADLE_ANDROID_LINT_TASK - name: Run Unit Test run: ./gradlew $GRADLE_UNIT_TEST_TASK - name: Set up and run Danger if: cancelled() != true # 中断されない限り、エラーでも実行 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 標準で利用できるトークンを利用 JOB_STATUS: ${{ job.status }} # jobのステータスを Danger へ受け渡す run: | gem install ${{ steps.gem-info.outputs.packages }} danger --dangerfile='.github/workflows/check-pull-request.danger' --remove-previous-comments --fail-on-errors=trueこのワークフローでは、Danger のプラグインとして danger-checkstyle_format、danger-android_lint、danger-junit を利用しています。
gem でインストールした Danger をコマンドラインで起動しています。コマンドラインのオプションについては GitHub の runner.rb ファイルを参照ください。(ドキュメントは見つけられませんでした。)
Danger 用のスクリプトファイルは次のものを用意し、上で説明したのワークフローの yaml ファイルと同じディレクトリに格納します。(こちらも、この記事の為にコメントを多めにいれています。)
.github/workflows/check-pull-request.danger# GitHub Actions の job のステータスを受け取る job_status = ENV['JOB_STATUS'] # 追加・変更していないコードはコメント対象外とするか github.dismiss_out_of_range_messages({ error: false, # エラーは追加・変更していないコードでもコメント warning: true, message: true, markdown: true }) # ktlint の結果ファイルの解析とコメント Dir.glob("**/build/reports/ktlint-results.xml").each { |report| checkstyle_format.base_path = Dir.pwd checkstyle_format.report report.to_s } # Android Lint の結果ファイルの解析とコメント Dir.glob("**/build/reports/lint-results*.xml").each { |report| android_lint.skip_gradle_task = true # 既にある結果ファイルを利用する android_lint.report_file = report.to_s android_lint.filtering = false # エラーは追加・変更したファイルでなくてもコメント android_lint.lint(inline_mode: true) # コードにインラインでコメントする } # 最終結果でレポートするワーニング数は Android Lint と ktlint のみの合計としたいのでここで変数に保存 lint_warning_count = status_report[:warnings].count # Local unit test の結果ファイルの解析とコメント Dir.glob("**/build/test-results/*/*.xml").each { |report| junit.parse report junit.show_skipped_tests = true # スキップしたテストをワーニングとする(状況により適宜変更) junit.report } # プルリクの body が空の場合はエラー fail 'Write at least one line in the description of PR.' if github.pr_body.length < 1 # プルリクが大きい場合はワーニング warn 'Changes have exceeded 500 lines. Divide if possible.' if git.lines_of_code > 500 # 追加で独自のチェックをする場合はこのあたりで実施する # ... # Danger でエラーがある場合は既に何かしらコメントされているのでここで終了 return unless status_report[:errors].empty? # GitHub Actions のワークフローのどこかでエラーがあった場合はその旨をコメントして終了 return markdown ':heavy_exclamation_mark:Pull request check failed.' if job_status != 'success' # 成功時のコメント(もし不要な場合は省いてもいいと思います) comment = ':heavy_check_mark:Pull request check passed.' if lint_warning_count == 0 markdown comment else # ktlint と Android Lint のワーニング数の合計をレポート markdown comment + " (But **#{lint_warning_count}** warnings reported by Android Lint and ktlint.)" end仮に Android プロジェクトのコンパイルが通らない場合は Android Lint あたりでエラーになるので、一応そこで気づくことができます
(ビルドできるかのステップを追加してチェックしてもよいのですが、そうすると時間がかかってしまうので追加していません。)
この2ファイルを含むブランチでプルリクエストを作成してみてください。そうするとプルリク上でワークフローが実行中の状態になります。
ワークフローの実行状況や結果は、上図の Details リンクから確認することができます。どこでエラーになったかもここで確認することができます。
Danger のコメント例をいくつか紹介します。ワーニング(
マークのコメント)に関しては、エラーと違い警告だけなので、いくらワーニングがあってもプルリクのチェック結果としては成功ステータスとなります。
ktlint または Android Lint のワーニング:
対象のコードにインラインでコメントされます。(ボットがレビューしているようで楽しいです
)
Android Lint のエラー:
Local unit test のエラー:
ワークフロー上で何かしらのエラーがある場合:
この場合はワークフローの実行結果画面からエラーの原因を探すことになります。(が、コンパイルエラーの場合が殆どです。)
エラーが無い場合:
成功の旨がコメントされます。
このように、GitHub Actions を利用することで、特別な CI サービスの契約なしに簡単に CI のワークフローを構築することができます。ワークフローがファイルとしてリポジトリで管理でき、実行状況や結果の確認も GitHub 内で完結しているのもいいですね
![]()
そのほかの設定
Gradle と gem のキャッシュの作成
紹介したワークフローでは actions/cache で Gradle と gem のキャッシュを利用するようにしていますが、キャッシュはブランチに紐づいています。プルリクで新しいブランチを作成して push しても、その新しいブランチには当然、キャッシュが存在しません。コードの追加 push 時にはキャッシュが適用されはするのですが、キャッシュを十分に利用できておらず微妙な感じです。
ただ actions/cache は、プルリクのブランチにキャッシュが無ければマージ先のブランチのキャッシュを、マージ先に無ければデフォルトブランチのキャッシュを探索し利用する仕様になっています。よって、それらのブランチに更新があったタイミングでキャッシュを作成するワークフローを用意しておけば、プルリクの新規作成のタイミングでもキャッシュを利用できる状態にすることができます
このワークフローは次のとおりです。(内容は先程のワークフローとだいたい同じなので細かい説明は省きます。)
.github/workflows/generate-cache.ymlname: Generate cache on: push: branches: # デフォルトブランチや主たるマージ先ブランチを指定 - master - develop* jobs: create: name: Generate cache runs-on: ubuntu-18.04 steps: - name: Check out uses: actions/checkout@v2 - name: Set up JDK uses: actions/setup-java@v1 with: java-version: 1.8 - name: Restore gradle cache id: gradle-cache uses: actions/cache@v2 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle', '**/*.gradle.kts') }} - name: Download dependencies if: steps.gradle-cache.outputs.cache-hit != 'true' # キャッシュが無い場合だけ実行 run: ./gradlew androidDependencies - name: Set up Ruby uses: actions/setup-ruby@v1 with: ruby-version: '2.6' - name: Get gem info env: PACKAGES: danger:6.2.0 danger-checkstyle_format:0.1.1 danger-android_lint:0.0.8 danger-junit:1.0.0 id: gem-info run: | echo "::set-output name=dir::$(gem environment gemdir)" echo "::set-output name=packages::$PACKAGES" echo "::set-output name=key::$(echo $PACKAGES | tr ' ' '-')" - name: Restore gem cache id: gem-cache uses: actions/cache@v2 with: path: ${{ steps.gem-info.outputs.dir }} key: ${{ runner.os }}-gem-${{ steps.gem-info.outputs.key }} - name: Set up Danger if: steps.gem-cache.outputs.cache-hit != 'true' # キャッシュが無い場合だけ実行 run: | gem install ${{ steps.gem-info.outputs.packages }}ktlint の導入
基本的には ktlint の README に導入方法が書かれていますが、
- マルチモジュールへの対応
- checkstyle 形式のレポートの追加(Danger 用)
- ktlint からの指摘があっても CI(GitHub Actions)上で異常終了させない
というのを考慮して、プロジェクトルートの build.gradle(
:app
配下のではなく)に次のように記載します。build.gradle// ... subprojects { // 配下のモジュール全てに適用される configurations { ktlint } dependencies { ktlint "com.pinterest:ktlint:0.39.0" } task ktlint(type: JavaExec, group: "verification") { description = "Check Kotlin code style." main = "com.pinterest.ktlint.Main" classpath = configurations.ktlint args "--android", "--color", "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/reports/ktlint-results.xml", "src/**/*.kt" ignoreExitValue true } task ktlintFormat(type: JavaExec, group: "formatting") { description = "Fix Kotlin code style deviations." main = "com.pinterest.ktlint.Main" classpath = configurations.ktlint args "-F", "--android", "src/**/*.kt" ignoreExitValue true } afterEvaluate { check.dependsOn ktlint } }ktlint から指摘があったらワークフローを失敗ステータスにする厳しい運用にする場合は
ignoreExitValue true
の記載は削除してください。もしモジュールによって動作を変えたい場合は、
it.name
でモジュール名が取得できるので、判定処理をいれるとよいでしょう。例えばライブラリ導入用のモジュールがあったりすると、Gradle の check タスクがなくエラーになったりするので、次のような判定で回避します。afterEvaluate { if (it.name != "awesome_module") { check.dependsOn ktlint } }次に、プロジェクトルートに .editorconfig ファイルを用意して、ktlint の追加の設定をします。
.editorconfig[*] insert_final_newline = true [*.{kt, kts}] max_line_length = 128 disabled_rules = import-ordering
insert_final_newline
を有効にすると、ファイルの末尾が改行文字であるかチェックされるようになります。Android Studio のコードフォーマッタもこの設定を参照しているので、ファイルの末尾が改行文字ではない場合は自動的に補完されるようになります。(この例では、Java のファイルなど、Kotlin 以外のファイルでも自動的に補完したいので[*]
で全ファイルを対象にしています。)
max_line_length
は、コード一行の最大文字数の設定です。デフォルトだと100
なのですが、弊社では128
にしています。このあたりはプロジェクトによって調整してください。
disabled_rules
には、無効にする ktlint のルールを指定します。この例ではimport-ordering
(コードの import 文の並び順のチェック)を無効にしています。現状、Android Studio のコードフォーマッタで並び替えた順序が ktlint の指摘の対象になってしまうからです。このあたり、最新バージョンの ktlint を利用(私が試したのは0.39.0
まで)したり、フォーマッタの設定を調整したり、.editorconfig ファイルを調整したりすれば回避できるのかもしれません。(私は何をしても上手くいきませんでした..)また、Android Studio のコードフォーマッタも、なるべく ktlint の指摘が少なくなるように設定されてあるものを利用する方がよいでしょう。おすすめの設定は ktlint の README に書いてあります。
プルリクのマージのブロック
リポジトリの Branch protection rules の設定で、今回のワークフローのジョブ(
Check pull request
)のステータスをチェックするようにします。ワークフローの実行中やエラー時はマージボタンを非活性にすることができるので、中途半端な状態でマージされるのを防げます。Danger(この画面での表示は
danger/danger
)もステータスを更新しますが、Danger でエラーの場合は今回のワークフローもエラーになるので、ここでの Danger のステータスのチェックは特に不要です。おわりに
いかがでしたでしょうか。今回は、ゆめみの Android プロジェクトでよく導入しているプルリクチェックのワークフローを紹介させて頂きました。他にも紹介したいワークフローはまだまだあるのですが、それはまた別の機会に紹介させて頂きます
- 投稿日:2020-12-16T07:01:59+09:00
Android11でメディアコントロールが出なかった。
結論
きちんとMediaSessionにメタ情報を設定することでメディアコントロールが出ましたので、きちんとメタ情報が設定されているか確認をしましょう。
mediaMetadata = new MediaMetadataCompat.Builder(); .putString(MediaMetadata.METADATA_KEY_TITLE, title) .putString(MediaMetadata.METADATA_KEY_ARTIST, artistName) .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, imageUri)) .putLong(MediaMetadata.METADATA_KEY_DURATION, duration) .build(); mediaSession.setMetadata(mediaMetadata)また
putLong(MediaMetadataCompat.METADATA_KEY_DURATION, -1);
とすることで、シークバーを無効化することができますが、シークバー自体を消すことはできないようです。経緯
AndroidにはMediaSessionという、外部デバイスなどからメディアが操作されたことをうまく制御する仕組みがあるのですが、
その中で、再生情報はMediaMetadataで管理するということになっています。
(詳しい記事はたくさんあるのでここでは詳しい解説は省きます。)しかし、MediaMetadataとは別にデータを管理していたため設定せずに完結していたこと。
メタ情報を設定せずともAndroid11以前はプレイヤーの表示・操作ができていたため、メタ情報の設定がされていないことに気付きにくかった。以上のことからメタ情報の設定がされていなかったことに現在まで気づくことができていませんでした。
最低限必要なメタ情報とメディアコントロールの表示の調査
Notificationのタイトル情報とMediaMetadataの関係を調査したところ以下のことがわかりました。
- MediaMetadataに対応するデータがある場合メタ情報が表示に使用される。
- 対応するメタ情報がないときはNotificationへ設定した値が表示される。
- MediaMetadataの中身が空っぽでも、Notificationに設定した情報が表示される。
インスタンスを生成して渡すだけでNotificationの情報から表示がされるようですが、しっかりと設定しましょう。
あとがき
本当はAndroid11から追加されたバブルに触れてみたのですが、ギリギリまで試行錯誤して、
一部端末でバブルの機能が動かないという報告を発見して断念しました。
そちらについては確認次第また別の機会に記事にしようと思います。
- 投稿日:2020-12-16T00:01:27+09:00
M5Stack遠隔押印装置
はじめに
リモートワークが増えている中で押印してもらうためだけに出社することがあります。世間では判子をなくそうという風潮です。しかし、判子は日本の文化だと思います。
- 自分の判子をもらったときの嬉しさ
- 上司になって押印したときの責任感
時代は電子承認の流れですが、1クリックで電子書類に印影が入るだけじゃダメなんです!
物理的に判子を押す感覚が欲しいんです!ということで、物理的に判子を押して、無駄にクラウドを使用して電子承認する仕組みを作りました。
開発環境
- M5Stack
- 感圧センサー(円形・大)
- 判子(今回は鈴木さん限定)
- 押印用スタンプ台
- 承認欄(この裏に感圧センサーを仕込む)
- Firebase Realtime Database
- Android端末(Nexus7)
構成図と押印の仕組み
構成図
承認依頼と押印フロー
- Android端末がRealtime Databaseから依頼を監視します。
- 依頼があると音声案内されます。
- 承認する場合は、承認蘭(物理)で押印します。
ここで押印の強さと時間を計測して、押印の濃さに利用します。- 押印が確定したら、Realtime Databaseへ押印情報(結果・強さ・時間など)を書き込みます。
- Android端末が承認情報を監視しているので、押印情報から画面へ押印します。
押印の強さと時間により押印の濃さが変わります。押印検出
- 感圧センサーをADして開始閾値(0.5V)超えたら押印開始と判断します。
- 完了閾値(0.2V)未満なら押印完了と判断します。
- 押印監視から押印完了までを押印時間(msec)とします。
- 押印の強さの判定用に最大電圧を保存しておきます。
Realtime Databaseデータ構造
Realtime Databaseのデータ構造をAndroidのコードから¥説明します。
data class ApprovalStatus( var status : Int = 0, // 押印ステータス(0:初期状態 1:押印開始 2:押印完了) var result : Int = 0, // 承認結果(0:否認 1:承認) var type : Int = 0, // 押印タイプ(0:真っ直ぐ 1:お辞儀 2:逆さま) var power : Float = 0.0f, // 押印力(V) var presstime : Int = 0 // 押印時間(msec) )
- 押印ステータスは、承認依頼がきたときに0、M5Stackで押印完了したときに2です。(1は未使用)
- 押印タイプは、現在M5Stackからは0固定です。(Androidアプリ側で制御)
- 押印力は、最大電圧値を設定します。
- 押印時間は、押印開始を検出してから押印完了までの時間を設定します。
ソースコード
M5StackとAndroid(承認取得と表示)のソースコードを説明します。
M5Stack
定義など
- AD計測ピン番号、押印検出状態、Firebase接続情報を定義します。
#include <M5Stack.h> #include <WiFi.h> #include <WiFiClientSecure.h> // 定義関連 // AD計測ピン番号 #define ANALOG_READ_PIN 35 #define TH_PUSH_DETECT (0.5) // 押印検出閾値(V) #define TH_PUSH_FINISH (0.2) // 押印終了閾値(V) // 押印検出状態 enum StatusDetect { DETECT_INIT = 0, // 初期状態 DETECT_START = 1, // 検出開始 DETECT_FINISH = 2 // 検出終了 }; StatusDetect statusDetect = DETECT_INIT; // 押印検出状態 unsigned long timeDetectStart = 0; // 押印検出開始時間(msec) unsigned long timeDetectEnd = 0; // 押印検出終了時間(msec) float peekVoltage = 0.0; // 最大電圧 // WiFiクライント(Firebase通信で使用) WiFiClientSecure client; // Firebase接続先 const char* firebase_host = "xxxx.firebaseio.com"; // Firebaseのシークレットキー const char* firebase_secrets = "xxxx"; // Realtime Database保存パス const char* path_hankoplus = "hankoplus/status"; // 押印検出情報初期化 void initDetect() { statusDetect = DETECT_INIT; timeDetectStart = 0; timeDetectEnd = 0; peekVoltage = 0.0; }初期化処理とメイン処理
- メイン処理では、押印検出を監視し、押印完了で送信データ8JSON)を生成してFirebaseへ保存します。
- Firebaseへの保存は下記のサイトを参考にさせていただきました。(詳細は割愛します)
Firebase Realtime Database のデータ保存、取得、ストリーミング受信実験( ESP32 , M5Stack )void setup() { M5.begin(); // スピーカーOFF M5.Speaker.write(0); // AD計測ピン設定 pinMode(ANALOG_READ_PIN, ANALOG); // Wifi接続(省略) // 押印検出情報初期化 initDetect(); } void loop() { M5.update(); // 押印計測処理 measure(); // 押印終了していればFirebaseへ保存 if (statusDetect == DETECT_FINISH) { int statusPush = 2; // 2:完了 int result = 1; // 1:成功 int typePush = 0; // 真っ直ぐ unsigned long presstime = timeDetectEnd - timeDetectStart; // 送信データ(JSON)生成 String body = ""; body += "{\n"; body += "\"status\":" + String(statusPush) + ",\n"; body += "\"type\":" + String(typePush) + ",\n"; body += "\"result\":" + String(result) + ",\n"; body += "\"power\":" + String(peekVoltage) + ",\n"; body += "\"presstime\":" + String(presstime) + "\n"; body += "}"; // Firebaseへ保存 postServerSentEvents(body); } }感圧センサーの押印検出処理
- 感圧センサーからの電圧を計測して開始閾値超えを検出したら、押印を開始したと判断します。
- 電圧が完了閾値未満を検出したら、押印が完了したと判断します。
- 押印開始から押印完了までの時間(msec)と最大値(V)を記憶しておきます。(Firebaseへ保存時に使用)
// 押印計測処理 void measure() { int val = 0; float voltage = 0.0; float voltageTotal = 0.0; float voltageAvg = 0.0; int count = 100; // 電圧を100回計測して平均化 for (int i = 0; i < count; i++) { val = analogRead(ANALOG_READ_PIN); voltage = (float)(val) * (3.3 / 4096.0); voltageTotal += voltage; delay(1); } voltageAvg = voltageTotal / (float)count; switch (statusDetect) { case DETECT_INIT: // 検出閾値を超えるまで待つ if (voltageAvg > TH_PUSH_DETECT) { statusDetect = DETECT_START; // 押印検出状態へ遷移 timeDetectStart = millis(); // 検出開始時間を保存 peekVoltage = voltageAvg; // 最大値を保存 } break; case DETECT_START: // 最大値チェック if (voltageAvg > peekVoltage) { peekVoltage = voltageAvg; // 最大値を保存 } // 検出閾値を下回るまで待つ if (voltageAvg < TH_PUSH_FINISH) { statusDetect = DETECT_FINISH; // 検出終了へ遷移 timeDetectEnd = millis(); // 検出終了時間を保存 } break; default: break; }Android(承認取得と表示)
Androidは承認状態の監視と印影画像作成の処理のみ示します。
承認状態の監視
- 承認状態(ApprovalStatus.status)が押印完了(2)になるのを監視します。
- modestampが0のときは真っ直ぐな印影(社長は必ずこれ)、1のときはお辞儀した印影を表示します。
// 承認状態監視Listener private val listenerApprovalStatus = object : ChildEventListener { override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) { // hankoplus/statusの変更あり val value = snapshot.getValue(ApprovalStatus::class.java) value?.let { // 承認状態で分岐 when (it.status) { 0 -> {} // 初期状態 1 -> {} // 押印判定開始 2 -> { // 押印完了(承認/否認いずれも) if (it.result == 1) { // 押印パワー取得(最大2.0Vとする) var power = it.power if (power > 2.0f) { power = 2.0f } // 押印時間取得 val presstime = it.presstime // 押印濃度計算(割合 -> パワー=0.4 時間=0.6) val arph = (((power / 2.0f) * 0.4f + (presstime.toFloat() / 8000.0f) * 0.6f) * 256).toInt() var bmp: Bitmap? = null if (modeStamp == 0) { // 社長は必ず真っ直ぐ bmp = createApproval("鈴木", 0, arph) } else { bmp = createApproval("鈴木", typeStamp, arph) } setApproval(bmp) // 次の承認ように承認状態を初期化 val updates = mutableMapOf<String, Any?>("status" to 0) refApprovalStatus?.child("status")?.updateChildren(updates) } } } } } override fun onCancelled(databaseError: DatabaseError) {} override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {} override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {} override fun onChildRemoved(snapshot: DataSnapshot) {} }印影画像の作成
- 押印濃度を計算したアルファ値で印影画像を作成します。
計算式は、押印した実験結果から独自に決定したものです。- Android端末(Nexus7用)と承認用紙の画面に合わせて作成してます。
// 承認画像の作成 private fun createApproval(name: String, type: Int, arph: Int): Bitmap { val width = 400 val height = 400 val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas() canvas.setBitmap(bmp) val paintBackground = Paint() paintBackground.setColor(Color.WHITE) val rect = Rect(0, 0, width, height) canvas.drawRect(rect, paintBackground) val paintName = Paint() paintName.textSize = 170.0f paintName.isAntiAlias = true paintName.setColor(Color.argb(arph, 255, 0, 0)) val paintFrame = Paint() paintFrame.setColor(Color.argb(arph, 255, 0, 0)) paintFrame.style = Paint.Style.STROKE paintFrame.strokeWidth = 16.0f val cx = width / 2.0f val cy = height / 2.0f val radius = (width * 0.9f) / 2.0f canvas.drawCircle(cx, cy, radius , paintFrame) var str = name.get(0).toString() canvas.drawText(str, 115.0f, 175.0f, paintName) str = name.get(1).toString() canvas.drawText(str, 115.0f, 340.0f, paintName) val mat = Matrix() when (type) { 0 -> { // 真っ直ぐ mat.postRotate(0.0f, cx, cy) } 1 -> { // お辞儀 mat.postRotate(-30.0f, cx, cy) } 2 -> { // 逆さま mat.postRotate(180.0f, cx, cy) } } val bmpMat = Bitmap.createBitmap(bmp, 0, 0, width, height, mat, true) val bmpStamp = Bitmap.createBitmap(bmpMat, (bmpMat.width / 2.0f - width / 2.0f).toInt(), (bmpMat.height / 2.0f - height / 2.0f).toInt(), width, height) return bmpStamp }参考サイト
Firebase Realtime Database のデータ保存、取得、ストリーミング受信実験( ESP32 , M5Stack )
おわりに
物理判子を押すというアナログな操作をM5Stackでデジタルに変換して、Firebaseでクラウドを使ってタブレットと連携までまるっとできるのは楽しいです。
開発当初は、押印方向(真っ直ぐ、お辞儀、逆さま)をM5Stackのボタンで選択していましたが、試験してもらったときに操作が面倒だったので、現在は承認書類が押印方向の情報を持っていて書類によりきまるようにしています。(デモ動画では書類の内容や情報は内部で固定ですが)
現在は鈴木さん限定なので、いろいろなお名前やオリジナル印影にも挑戦したいですね。M5Stackはセンサー類も充実していますし、感圧センサーのような変化を電圧値として計測できれば、いろいろと応用ができるのが楽しいので、今後もネタかつ実用的なものを作っていきたいです。