20200214のAndroidに関する記事は8件です。

Android 10 (API 29)対応で画像や動画ファイルをサーバに送信させたときの話

targetSdkVersionを29したときに画像のサーバ送信で苦戦したので備忘録として

問題点

https://developer.android.com/training/data-storage/files/external-scoped

「ファイル アクセスに必要なパーミッション」の項にあるこの部分

READ_EXTERNAL_STORAGE パーミッションが付与されていたとしても、外部ストレージ デバイスの未加工ファイル システム ビューにアクセスするアプリの場合、アクセスできるのは、アプリ固有ディレクトリに限られます。アプリが未加工ファイル システム ビューを使用してアプリ固有ディレクトリの外部にあるファイルを開こうとすると、エラーが発生します

どうやら
「自分が作成した画像(動画)以外は使っちゃだめよ!
ユーザが画像を選択後に、内部的に別のアプリがその画像を加工してたとしたら大変でしょ?(意訳」
という事らしい

一応逃げ道として

アプリが対象範囲別ストレージに完全に対応するまでは、アプリのターゲット SDK レベルや requestLegacyExternalStorage マニフェスト属性に基づいて、一時的に対象範囲別ストレージ機能を無効にすることができます。

とあるがいつまで使えるかはよくわからん

要件まとめ

・targetSdkVersionを29に変更する
・画像をサーバーに送れるようにする
・アプリは複数ある(minSdkVersionはまちまち
)
・すぐに更新しなければならなくなる事を考えて requestLegacyExternalStorage は使いたくない

解決策

選択したファイルを自アプリのキャッシュに一度保存してそかからサーバにアップする

苦戦した事

・minSdkVersionがまちまちのため、使える手段が少なかった(Paths も Files も使えない)
・FileInputStreamしただけでも怒られる
・1Byteづつコピーするのは時間がかかりすぎる

実際の対応

URIからInputStreamを作ってそっからChannelをかませて自アプリのキャッシュDirに出力した

実際の実装(一部抜粋)

        cacheFile = new File(activity.getCacheDir(), fileName);
        try {
            InputStream inputStream = activity.getContentResolver().openInputStream(fileUri);

            FileOutputStream fOutputStream = new FileOutputStream(cacheFile);


            try {
                fOutputStream.getChannel().transferFrom(Channels.newChannel(inputStream), 0, Long.MAX_VALUE);
            } finally {
                fOutputStream.close();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

        return cacheFile.getPath();

最後に

色々長時間ごにょごにょしてた気がするけど終わってみれば「こんなものですむのか・・・」といったエンジニアあるある

何かご指摘事項等ありましたらお手柔らかにお願いします

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Build React Native Custom Checkbox Component for Android and iOS

In this tutorial, we are going to learn how to create a custom Checkbox component in React Native application for Android and iOS platforms. We will learn step by step how to get Multiple checkboxes values on button click using React Native APIs.

READ MORE TO CLICKHERE

https://www.positronx.io/build-react-native-custom-checkbox-for-android-and-ios/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】テキストエディタ(EditText)にアンドゥ・リドゥ機能を付ける

概要

AndroidアプリのEditTextで、アンドゥ(元に戻す)・リドゥ(やり直し)ができるようにしたかった。しかし、標準ライブラリではアンドゥ・リドゥ機能は用意されていないらしい(多分)。
アンドゥ・リドゥを実装する方法を調べた。

アンドゥ・リドゥを実現する方法

android.textパッケージにTextWatcherというインターフェースが用意されているため、それを利用して実装する。

TextWatcherとは

When an object of this type is attached to an Editable, its methods will be called when the text is changed.

Editableにこの型のオブジェクトが付いているとき、テキストが変更されたときにTextWatcherのメソッドが呼ばれる。
呼ばれるメソッドは以下の3つである。TextWatcherはインターフェースであるため、3つともオーバーライドする必要がある。

  • beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int)
    s中のstart番目から始まるcount個の文字が、長さafterの新しいテキストに置き換えられようとしているときに呼ばれるメソッド
  • onTextChanged(s: CharSequence, start: Int, before: Int, count: Int)
    s中のstart番目から始まるcount個の文字が、長さbeforeの古いテキストを置き換えたときに呼ばれるメソッド
  • afterTextChanged(s: Editable)
    s中のどこかで、テキストが変更されたときに呼ばれるメソッド
UndoTextWatcher.kt
class UndoTextWatcher : TextWatcher {
    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
        // テキスト変更が起こる直前の処理(undo用の文字列保持など)
    }

    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
        // テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など)
    }

    override fun afterTextChanged(s: Editable) {
        // テキストが変更された後の処理(undo・redoの可否判定など)
    }
}

例えば「あいうえお」→「あいうえおかきくけこ」の場合は以下のようになる。

log
System.out: beforeTextChanged // (「お」の直後の)0文字が5文字のafterに置き換えられようとしている
System.out: s: あいうえお, start: 5, count: 0, after: 5

System.out: onTextChanged // 「か」から5文字が0文字のbeforeを置き換えた
System.out: s: あいうえおかきくけこ, start: 5, before: 0, count: 5

System.out: afterTextChanged // 「あいうえおかきくけこ」になった
System.out: s: あいうえおかきくけこ

挙動を実装する

まず、undoing(アンドゥ・リドゥ実行中かどうか)・undos(アンドゥのリスト)・redos(リドゥのリスト)と、beforeafterを保持するクラスを用意しておく。

UndoTextWatcher.kt
class EditEvent {
    private val beforePosition: Int // 置き換えられる文字列の開始位置
    private val before: CharSequence // 置き換えられる文字列
    private var afterPosition: Int = 0 // 置き換えた文字列の開始位置
    private var after: CharSequence = "" // 置き換えた文字列

    constructor(beforePosition: Int, before: CharSequence) {
        this.beforePosition = beforePosition
        this.before = before
    }

    fun setAfter(afterPosition: Int, after: CharSequence) {
        this.afterPosition = afterPosition
        this.after = after
    }

    fun undo(editable: Editable) {
        editable.replace(afterPosition, afterPosition + after.length, before)
    }

    fun redo(editable: Editable) {
        editable.replace(beforePosition, beforePosition + before.length, after)
    }
}

class UndoTextWatcher : TextWatcher {
    var undoing = false // undo・redo実行中かどうか
    val undos = LinkedList<EditEvent>() // undoリスト
    val redos = LinkedList<EditEvent>() // redoリスト

    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
        // テキスト変更が起こる直前の処理(undo用の文字列保持など)
    }

    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
        // テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など)
    }

    override fun afterTextChanged(s: Editable) {
        // テキストが変更された後の処理(undo・redoの可否判定など)
    }
}

beforeTextChangedでやること

[0] (undoingtrueのとき、何もしない。)
[1] startと、元文字列sstartからcount文字(==置き換えられる文字列)をundosに追加する。
[2] redosが空でない場合、redosを空にする。

UndoTextWatcher.kt
class UndoTextWatcher : TextWatcher {
    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
        // テキスト変更が起こる直前の処理(undo用の文字列保持など)
        if (undoing) return // [0]
        val event = EditEvent(start, s.subSequence(start, start + count)) // undoする文字列と位置を保持する
        undos.addLast(event) // [1]
        clearRedos() // [2]
    }

    private fun clearRedos() {
        while (!redos.isEmpty()) {
            redos.removeFirst()
        }
    }
    ...()...
}

onTextChangedでやること

[0] (undoingtrueのとき、何もしない。)
[1] beforeTextChangedundosに追加した要素に、startと、新文字列sstartからcount文字(==置き換えた文字列)の情報を追加する。

UndoTextWatcher.kt
class UndoTextWatcher : TextWatcher {
    ...()...
    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
        // テキスト変更が起きた直後の処理(undoしたときに置き換える箇所の取得・保持など)
        if (undoing) return // [0]
        val event = undos.getLast() // undoリストに最後に追加した要素
        event.setAfter(start, s.subSequence(start, start + count)) // [1]
    }
    ...()...
}

アンドゥボタンが押されたときにやること

[0] (undosが空の場合何もしない。)
[1] undosから最新の要素を取り出して、redosに追加する。
[2] undoingtrueにする。
[3] 文字列全体のafterPositionからafterの長さ分の文字列(つまりafter)を、beforeに置き換える。
[4] undoingfalseにする。

UndoTextWatcher.kt
class UndoTextWatcher : TextWatcher {
    ...()...
    fun undo(editable: Editable) {
        if (undos.isEmpty()) return // [0]
        val event = undos.removeLast() // [1]
        redos.addLast(event) // [1]
        undoing = true // [2]
        try {
            event.undo(editable) // [3]
        } finally {
            undoing = false // [4]
        }
    }
}

リドゥボタンが押されたときにやること

[0] (redosが空の場合何もしない。)
[1] redosから最新の要素を取り出して、undosに追加する。
[2] undoingtrueにする。
[3] 文字列全体のbeforePositionからbeforeの長さ分の文字列(つまりbefore)を、afterに置き換える。
[4] undoingfalseにする。

UndoTextWatcher.kt
class UndoTextWatcher : TextWatcher {
    ...()...
    fun redo(editable: Editable) {
        if (redos.isEmpty()) return // [0]
        val event = redos.removeLast() // [1]
        undos.addLast(event) // [1]
        undoing = true // [2]
        try {
            event.redo(editable) // [3]
        } finally {
            undoing = false // [4]
        }
    }
}

これでアンドゥ・リドゥ機能の挙動の実装は完成した。

EditTextで使えるようにする

実際にアプリの画面でアンドゥ・リドゥできるようにする。
EditText・undoボタン・redoボタンは適当に用意しておく。

Fragment.kt
val textWatcher = UndoTextWatcher()
editText.addTextChangedListener(textWatcher) // undo・redo機能を付けたTextWatcherをEditTextにセットする
undoButton.setOnClickListener {
    textWatcher.undo(editText.text) // undoボタンを押してundoする
}
redoButton.setOnClickListener {
    textWatcher.redo(editText.text) // redoボタンを押してredoする
}

これで、アプリ上でアンドゥ・リドゥを実行できるようになる。

その他

アンドゥ・リドゥできるか判定する

これ以上アンドゥできない時にボタンを無効にしたいなどの場合に、アンドゥできるかどうかを判定する。

スクリーンショット 2020-02-14 18.38.18.png
↑アンドゥボタンが無効になるようにした

Fragment.kt
class UndoTextWatcher : TextWatcher {
    ...()...
    override fun afterTextChanged(s: Editable) {
        hasUndos = !undos.isEmpty() // undoできるか(undoリストが空でないか)
        hasRedos = !redos.isEmpty() // redoできるか(redoリストが空でないか)
    }
}

if (!hasUndos) {
    // ボタン無効などの処理
}
if (!hasRedos) {
    // ボタン無効などの処理
}

まとめ

  • TextWatcherを利用して置き換える文字列や位置を取得し、アンドゥ・リドゥ機能を実装した。
  • EditText.addTextChangedListener()TextWatcherをセットすると、アプリ上でアンドゥ・リドゥが使えるようになる。

参考

https://github.com/tkojitu/UndoDemo

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【kotlin/Android】三目並べを作ってみよう!

学生時代では、Java + eclipse環境で、RPGのAndroidアプリを作成した経験がありますが、
最近のAndroidプログラミング言語はKorlin、開発環境はAndroid Studioが主流になっているみたいです。
スマホアプリ作りたくなったので、言語と環境を覚えるために、Kotlinでサンプルの三目並べゲームを作ってみました。

activity_main.xml

アプリの画面のレイアウトを記載する場所です。
activity_main.jpg

赤いエリア    → palette。部品を選択するところです。
水色のエリア   → Component Tree。画面に設置した部品の関係ツリーを表示します。
緑のエリア    → スマホの画面。ここに部品を配置します。
オレンジのエリア → 緑色のエリアで選択している部品のプロパティが表示されます。

今回はTable LayoutにTable Rowを配置してその中にbuttonを配置して、三目並べの形を作りました!
それで、それぞれのbuttonに、onClickイベントがあるので、ここで呼び出すfunction名を書き込むと、
MainActivity.kt側のfunctionを呼び出すことができるようになります。

onclick.jpg

コードはこちら

<?xml version="1.0" encoding="utf-8"?>
<TableLayout 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"
    android:background="@color/whiteColorBackground"
    android:gravity="center"
    tools:context=".MainActivity">

    <TableRow
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_marginTop="3px">

        <Button
            android:id="@+id/bu1"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />

        <Button
            android:id="@+id/bu2"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />

        <Button
            android:id="@+id/bu3"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />
    </TableRow>


    <TableRow
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_marginTop="3px">

        <Button
            android:id="@+id/bu4"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />

        <Button
            android:id="@+id/bu5"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />

        <Button
            android:id="@+id/bu6"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />
    </TableRow>

    <TableRow
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_marginTop="3px">

        <Button
            android:id="@+id/bu7"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />

        <Button
            android:id="@+id/bu8"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />

        <Button
            android:id="@+id/bu9"
            android:layout_width="40pt"
            android:layout_height="40pt"
            android:layout_marginLeft="3pt"
            android:background="@color/whiteBu"
            android:onClick="buClick" />
    </TableRow>

</TableLayout>

colors.xml

先程の、activity_main.xmlでbackgroundに色を指定しているのですが、その色は直接色コードを指定する他に、
名前をつけて定義することもできます。
こちらのほうが変更するときに後々楽なので定義します。
backglound.png

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
    <color name="whiteColorBackground">#D3D3D3</color>
    <color name="whiteBu">#F9F9F9</color>
    <color name="blue">#FF33B5E5</color>
    <color name="darkGreen">#FF669900</color>
</resources>

MainActivity.kt

実行コードです。

ボタンを押されたとき、buClickイベントが実行されてゲームが進みます。

package com.example.tictactoeapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import java.util.Random

class MainActivity : AppCompatActivity() {

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

    fun buClick(view: View) {
        val buSelected =  view as Button

        var cellId = 0
        when(buSelected.id){
            R.id.bu1 -> cellId = 1
            R.id.bu2 -> cellId = 2
            R.id.bu3 -> cellId = 3
            R.id.bu4 -> cellId = 4
            R.id.bu5 -> cellId = 5
            R.id.bu6 -> cellId = 6
            R.id.bu7 -> cellId = 7
            R.id.bu8 -> cellId = 8
            R.id.bu9 -> cellId = 9
        }
        //Log.d("buClick", buSelected.id.toString())
        //Log.d("buClick: cellId", cellId.toString())

        playGame(cellId, buSelected)
    }

    var activityPlayer = 1

    var player1 = ArrayList<Int>()
    var player2 = ArrayList<Int>()


    fun playGame(cellId:Int, buSelected:Button){

        if(activityPlayer == 1){
            buSelected.text = "X"
            buSelected.setBackgroundResource(R.color.blue)
            player1.add(cellId)
            activityPlayer = 2
            autoPlay()
        }else{
            buSelected.text = "O"
            buSelected.setBackgroundResource(R.color.darkGreen)
            player2.add(cellId)
            activityPlayer = 1
        }
        buSelected.isEnabled = false

        checkWinner()

    }

    fun checkWinner(){
        var winner = -1

        //row1
        if(player1.contains(1) && player1.contains(2) && player1.contains(3)) {
            winner = 1
        }

        if(player2.contains(1) && player2.contains(2) && player2.contains(3)){
            winner = 2
        }

        //row2
        if(player1.contains(4) && player1.contains(5) && player1.contains(6)) {
            winner = 1
        }

        if(player2.contains(4) && player2.contains(5) && player2.contains(6)){
            winner = 2
        }

        //row3
        if(player1.contains(7) && player1.contains(8) && player1.contains(9)) {
            winner = 1
        }

        if(player2.contains(7) && player2.contains(8) && player2.contains(9)){
            winner = 2
        }

        //col1
        if(player1.contains(1) && player1.contains(4) && player1.contains(7)) {
            winner = 1
        }

        if(player2.contains(1) && player2.contains(4) && player2.contains(7)){
            winner = 2
        }

        //col2
        if(player1.contains(2) && player1.contains(5) && player1.contains(8)) {
            winner = 1
        }

        if(player2.contains(2) && player2.contains(5) && player2.contains(8)){
            winner = 2
        }

        //col3
        if(player1.contains(3) && player1.contains(6) && player1.contains(9)) {
            winner = 1
        }

        if(player2.contains(3) && player2.contains(6) && player2.contains(9)){
            winner = 2
        }

        //cross1
        if(player1.contains(1) && player1.contains(5) && player1.contains(9)) {
            winner = 1
        }

        if(player2.contains(1) && player2.contains(5) && player2.contains(9)){
            winner = 2
        }

        //cross2
        if(player1.contains(3) && player1.contains(5) && player1.contains(7)) {
            winner = 1
        }

        if(player2.contains(3) && player2.contains(5) && player2.contains(7)){
            winner = 2
        }


        if(winner == 1){
            player1WinnerCount += 1
            Toast.makeText(this, "Player 1 win the game", Toast.LENGTH_LONG).show()
            restartGame()
        }else if(winner == 2){
            player2WinnerCount += 1
            Toast.makeText(this, "Player 2 win the game", Toast.LENGTH_LONG).show()
            restartGame()

        }
    }

    fun autoPlay(){
        var emptyCells = ArrayList<Int>()

        for(cellId in 1..9){
            if(!(player1.contains(cellId) || player2.contains(cellId))){
                emptyCells.add(cellId)
            }
        }

        if(emptyCells.size == 0){
            restartGame()
        }

        val r = Random()
        val randIndex = r.nextInt(emptyCells.size)
        val cellId = emptyCells[randIndex]

        var buSelected:Button?
        buSelected = when(cellId){
            1 -> bu1
            2 -> bu2
            3 -> bu3
            4 -> bu4
            5 -> bu5
            6 -> bu6
            7 -> bu7
            8 -> bu8
            9 -> bu9
            else -> {bu1}
        }

        playGame(cellId, buSelected)
    }

    var player1WinnerCount = 0
    var player2WinnerCount = 0

    fun restartGame(){
        activityPlayer = 1
        player1.clear()
        player2.clear()

        for(index in 1..9){
            val buSelected:Button
            buSelected = when(index){
                1 -> bu1
                2 -> bu2
                3 -> bu3
                4 -> bu4
                5 -> bu5
                6 -> bu6
                7 -> bu7
                8 -> bu8
                9 -> bu9
                else -> {bu1}
            }
            buSelected!!.text = ""
            buSelected!!.setBackgroundResource(R.color.whiteBu)
            buSelected!!.isEnabled = true
        }

        Toast.makeText(this, "Player1: $player1WinnerCount ,Player2: $player2WinnerCount", Toast.LENGTH_LONG).show()
    }
}

実行結果

エミュレーターで実行するとこんな感じです。

image.png

感想

AndroidStudioが登場したことで、Java+eclipseの時代よりも、簡単に開発環境の構築と
コーディングできるようになっていると思います。
kotlinについてはJavaのライブラリも最新のkotlinのライブラリも使えつつ、現代的なコードなので生産性の向上が期待できると思います。
もっと多くのアプリを作ってみたいところです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

16進数文字列から画像に変換する方法(Kotlin)

https://so-wh.at/entry/20041012/p1
上記のページを参考にしました。もしかしたら他の方が書いているかもしれませんが自分用メモとして。

前提として、画像を16進文字列に変換したテキストファイルをアセットに保管してあります。

fun hextoBitmap(file: String): Bitmap {
        val openfile = assets.open(file)
        var imagetext = ""
        try {
            val inst = InputStreamReader(openfile)
            val br = BufferedReader(inst)
            var line = br.readLine()
            while (line != null) {
                imagetext += line
                line = br.readLine()
            }
            inst.close()
            br.close()
        }catch (e:IOException) {
            Log.e("IOException", e.toString())
        }

        val bytes = mutableListOf<Byte>()
        for(index in 1..imagetext.length/2) {
            val byte = Integer.parseInt(imagetext.substring((index - 1) * 2,index * 2),16)
            bytes.add(byte.toByte())
        }
        println(bytes)

        val byteArray = bytes.toByteArray()

        val bitmap = BitmapFactory.decodeByteArray(bytearray, 0, byteArray.size)

        return bitmap
    }


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FrescoのSimpleDraweeViewで新しい画像をセットしても古い画像が表示された問題の解消

問題

Androidの開発でFrescoを使ってることは多いと思う。

そのFrescoのSimpleDraweeViewを使用していた際、新しい画像をセットしても画面に反映されない現象に遭遇した。

解決方法

キャッシュを消す。(by stack overflow)

Fresco.getImagePipeline().evictFromMemoryCache(uri)

解決までの流れ

R1. 新しくセットするuriをBitmap化して、そのuriがセットしたい画像かをDebugで確認する
・ 下記コードでbitmap変数を作成し、このコード直後にbreak pointを貼る。

val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)

R2. break pointで止まったら、ToolbarのDebugからbitmap変数のView Bitmapをクリックして画像を目視確認する。

R3. 画像がセットしたい画像なので、キャッシュを疑い消す。

R4. 解決。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ESP32をBluetoothキーボードにして楽譜めくり用フットペダルを作ってみる

概要

ESP32をBLEキーボードにするという記事を見かけました。
普段からAndroidタブレットで楽譜を見たりしているのですが、演奏中に使うと譜めくりが大変です。

以前に上のようなレゴブロックと特殊キーボードを使ったなんちゃってフットペダル(譜めくり用)を作ってみたことがあるのですが、こんな感じでESP32を使ってBluetooth接続のものにしたいと思います。

ESP32評価ボード(安いの)

ESP32はAmazonで見つけたのを適当に買いました。
(これ → https://t.co/1FKYttcJcp
買っては見たものの、ピン配置が他の評価ボードと同じかどうかもよく分からずなんとなく使ってます。
(どうせたいしたことはしないので)

ESP32への書き込みはArduino IDEを使ってますが、ここではArduino IDEのインストールやセットアップについては触れません。

BLEキーボードライブラリ

https://github.com/T-vK/ESP32-BLE-Keyboard
こちらを利用させていただきました。
ダウンロードしてきて、Arduino IDEのlibrariesフォルダ(普通にインストールするとこの辺? → C:\Program Files (x86)\Arduino\libraries\)に入れて完了。
(これ以降の記事ほぼ不要)

プログラム

まとまってないけどこんな感じ。
ボタン2つで、'f' と 'b' を送るだけのもの。
キーイベントが送られてきた後はAndroid側でどうにかします。

#include <BleKeyboard.h>

#define PIN_LED0 2
#define PIN_BTN0 4
#define PIN_BTN1 5

BleKeyboard bleKeyboard;

int gConnect = 0;
int gBtn0 = HIGH;
int gBtn1 = HIGH;

uint8_t chrBtn0 = 'f';
uint8_t chrBtn1 = 'b';

void setup() {
  Serial.begin(115200);
  Serial.println("Starting BLE Keyboard");
  bleKeyboard.begin();

  pinMode(PIN_LED0, OUTPUT);
  pinMode(PIN_BTN0, INPUT_PULLUP);
  pinMode(PIN_BTN1, INPUT_PULLUP);
}

void loop() {
  int btn0 = digitalRead(PIN_BTN0);
  int btn1 = digitalRead(PIN_BTN1);

  if(bleKeyboard.isConnected()) {
    if(gConnect == 0){
      gConnect = 1;
      digitalWrite(PIN_LED0, HIGH);
      Serial.println("BLE Keyboard: connected");
    }

    if(gBtn0 != btn0){
      if(btn0 == LOW){
        Serial.println("Button 0");
        bleKeyboard.write(chrBtn0);
      }      
    }    
    if(gBtn1 != btn1){
      if(btn1 == LOW){
        Serial.println("Button 1");
        bleKeyboard.write(chrBtn1);
      }      
    }    
  }
  else {
    if(gConnect == 1){
      gConnect = 0;
      digitalWrite(PIN_LED0, LOW);
      Serial.println("BLE Keyboard: disconnected");
    }
  }

  gBtn0 = btn0;
  gBtn1 = btn1;

  delay(100);
}

PerfectViewer

Android側はこのソフトで楽譜を表示することにします。
漫画ビューアとして有名ですが、

  • 余白自動カット
  • ハードキー設定
  • スクロール量設定

どの機能をとっても今回の用途に最適です。
このソフトはきっと楽譜ビューアだと思います。

設定項目 設定値
余白自動カット ON
ハードキー設定 'f'→「次のページにスクロール」
'b'→「前のページにスクロール」
スクロール量設定 お好みで

こんな感じに設定。

回路

回路というのはばかられるくらい簡単。
GPIO4,5をINPUT_PULLUPにしているので、タクトスイッチを挟んでGPIO4,5とGNDを直結するだけ。ブレッドボード上だとこんな感じ。
KIMG0786.JPG

作ってみた

ESP32と電池とボタンを百均で買ってきた適当な大きさのケースに詰め込みます。
配線が余って大変なことになっていますが気にしません。

KIMG0801.JPG
KIMG0800.JPG

動かしてみた

実際に動かしてみたのがこちら。
ケースの角を押すとボタンが押されて楽譜がスクロールします。

まとめ

これくらいコンパクトで数個のボタンがついててカスタマイズ可能なフットペダル売ってないですかね、、?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MediaCodecとBluetoothを使ってミラーリングソフトを作る

目的

Android搭載ウェアラブル端末触る際に出力されてる画面を手元のスマホに写すようにすれば楽じゃね?ってのがスタート。
初Androidアプリ作成&初Kotlinなのでたくさん叩いてください。

今回は非同期処理になるのでMediaCodecのCallBackで処理します。
送信端末がZenfone5、受信側がPixel3で動作確認をしています。
OSのバージョンによっては修正が必要な箇所がでますが適宜書き換えてください。

使ったもの、参考にしたもの

流れ

送信側

  1. エンコーダの作成
  2. 解像度、FPSなどの指定
  3. コールバック関数の設定
  4. エンコーダに設定値反映
  5. エンコーダ入力用Surfaceの取得
  6. エンコーダの開始
  7. エンコーダが出力完了する度にコールバック関数が呼ばれ
  8. フレームごとにBluetoothで送信

受信側

  1. デコーダの作成
  2. 解像度、FPS、出力先などの指定
  3. エンコーダに設定値反映
  4. コールバック関数の設定
  5. デコーダの開始
  6. デコーダへ入力
  7. Surfaceが更新される

解説

送信側

エンコーダの作成

createEncoderByTypeでcodecのインスタンスを生成
戻り値にcodecインスタンス、引数には使用するエンコード方法を指定。
文字列で指定しますが定数があるので覚えなくても問題なし。
エンコード/デコードの種類

codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)

解像度、FPSなどの指定

MediaFormat.createVideoFormatでformatの作成。

format = MediaFormat.createVideoFormat(MIMETYPE, width, height)

引数にはエンコード方法、縦横の解像度を指定。
作成したformatにsetIntegerメソッドで足りていない情報を設定。
今回は

  • カラーフォーマット
  • ビットレート
  • FPS
  • キャプチャレート
  • Iフレームインターバル

を設定しています。
formatの設定値

format.setInteger(
    MediaFormat.KEY_COLOR_FORMAT,  // 設定するパラメータ
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface // 設定値
)

コールバック関数の設定

冒頭にも書きましたが非同期処理の場合はコールバックにて入出力のタイミングが通知される。
コーデックの抽象メソッドは下記4つ

// コーデックの入力バッファが使用可能になった時に呼ばれる
onInputBufferAvailable(codec: MediaCodec, index: Int)
// コーデックの出力バッファが使用可能になった時に呼ばれる
onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo)
// コーデックエラー時に呼ばれる
onError(codec: MediaCodec, e: MediaCodec.CodecException)
// bitrateやfps、解像度などフォーマット変更時に呼ばれる(ここは理解不足です)
onOutputFormatChanged(codec: MediaCodec, format: MediaFormat)

エンコード側は入力→Surfaceから自動取得、出力→フレームをBluetooth送信なので
onOutputBufferAvailableのみ実装、残りは空実装してください。

override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
    val buffer = codec.getOutputBuffer(index)
    val array = ByteArray(info.size + 4)

    buffer?.run {
        this.get(array, 0, info.size)
        // emulation_prevention_three_byte
        // 0x00000002はH264のNALパケットには出てこない
        array[0 + info.size] = 0.toByte()
        array[1 + info.size] = 0.toByte()
        array[2 + info.size] = 0.toByte()
        array[3 + info.size] = 2.toByte()
    }

    val intent = Intent()
    intent.action = "DISPLAY_UPDATE"
    intent.putExtra("IMAGE", array)
    // アプリ内のみに対してブロードキャスト送信
    val mBroadcastReceiver = LocalBroadcastManager.getInstance(context)
    mBroadcastReceiver.sendBroadcast(intent)

    // バッファを解放
    codec.releaseOutputBuffer(index, false)
}

codec.getOutputBuffer(index)でByteBuffer型のフレームデータを取得。
このデータがH264フォーマットの1フレーム分ですが、このまま投げると受信側で切れ目がわからなくなるのでフッタを追加。

codec.releaseOutputBuffer()でバッファの開放を忘れないこと。
第二引数をTrueにするとSurfaceへ出力される。エンコーダ側なのでFalse。

送るデータの準備ができたらBluetoothクラスに送信します!が理解不足のため、onOutputBufferAvailable内で送信することができないのでIntentでMainActivityに渡しています。

エンコーダに設定値反映

MediaFormat.createVideoFormatとformat.setIntegerで設定したformatをcodecに反映させる。

// エンコーダを設定
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

第一引数にformat、第二引数に出力先Surfaceを指定。
第四引数にはフラグを設定しますが、送信側(エンコーダ)の場合はCONFIGURE_FLAG_ENCODEを設定。

エンコーダ入力用Surfaceの取得

コーデック側の対応はcodec.createInputSurface()でSurfaceを取得しておくことのみ。

// エンコーダにフレームを渡すのに使うSurfaceを取得
// configureとstartの間で呼ぶ必要あり
surface =  codec.createInputSurface()

エンコーダの開始

設定とコールバックの実装が終わったらエンコーダの開始。
引数もなしにstart,のみ。
この時点ではコーデックになにも入力されていないため出力無し。

codec.start()

仮想ディスプレイの出力先を↑のSurfaceに指定

入力はSurafceを使うのでMediaProjectionを使ってAndroidの画面をキャプチャし続ける。
その辺りはAndroidの画面をPCにミラーリングするソフトを作る1を参考

エンコーダが出力完了する度にコールバック関数が呼ばれ、

フレームごとにBluetoothで送信

↑この2つはコールバック関数の設定のところに記載。
Bluetooth部分についてはBluetoothChatのコードや解説記事などがたくさんあるのでそちらを参照。

受信側

デコーダの作成

エンコーダの作成をほとんど同じです。

create En coderByTypeとcreate De coderByTypeの違いのみ。

もちろんコーデック方法があっていないとデコードできないので送信側と合わせる。

codec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)

解像度、FPS、出力先などの指定

ここもエンコーダとほとんど同じ。
デコーダは送られてくるデータを処理するだけなのでIフレームの設定など、エンコーダに比べて設定箇所は減る。

// この2つはエンコーダのみ必要
// format.setInteger(MediaFormat.KEY_FRAME_RATE, fps)
// format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)

コールバック関数の設定

デコーダ側は 入力→Bluetoothでフレーム受信、出力→Surfaceへ出力
onInputBufferAvailableとonOutputBufferAvailableを実装。

override fun onInputBufferAvailable(mc:MediaCodec, inputBufferId:Int) {
    codecBufId = inputBufferId
    codecInputBuffer = codec.getInputBuffer(inputBufferId)!!
    isCodecBufAvailabile = true
}

// 出力バッファ使用可能時(フレーム作成完了時)
override fun onOutputBufferAvailable(
    mc: MediaCodec,
    index: Int,
    info: MediaCodec.BufferInfo
) {
    if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
        codec.releaseOutputBuffer(index, false)
        return
    }

    // 第2引数がTrueならSurfaceに出力
    codec.releaseOutputBuffer(index, true)
}

入力処理は

  1. 入力バッファが使用可能になる(onInputBufferAvailableが呼ばれる)
  2. codecInputBuffer = codec.getInputBuffer()でバッファを取得
  3. codecInputBuffer.put(ByteArray, offset, size)でバッファにデータを格納
  4. codec.queueInputBuffer()でデコーダにデータをキューイング

今回は2まではonInputBufferAvailableで処理し、3,4はBluetoothの受信処理の方で行っている。

出力処理は

codec.releaseOutputBuffer(index, true)

のtrue,falseが肝。

infoをみてコンフィグデータだった場合は、falseを指定してSurfaceへ出力しないようにしている。
残りのフレームデータの場合はtrueを指定。
キーフレームとは?Iフレーム・Pフレーム・Bフレームの違い【GOP】

エンコーダに設定値反映

デコーダの場合は第二引数にSurfaceを出力先として指定、第四引数はエンコーダの時のみ指定なので0で良い。

codec.configure(format, surface, null, 0)

デコーダの開始

ここはエンコーダと同じ

codec.start()

デコーダへ入力

コールバック関数の設定でバッファを取得後、フレームデータを入力。

/* 

受信処理
・受信完了時、recvStateをWAIT_INPUTBUFFER_AVAILABLE
・ダブルバッファで2フレーム分保持できるようにしておく
    受信用:readBuf,確定した1フレーム分:queuingBuffer
*/

// 入力バッファ使用許可 + 入力フレーム格納済みの場合
if(isCodecBufAvailabile && recvState == WAIT_INPUTBUFFER_AVAILABLE) {
    // フラグOff デコーダのCallbackによってOnされる
    isCodecBufAvailabile = false

    val bufDataSize = queuingBuffer.position()
    // 1フレーム分をデコーダのバッファにコピー
    codecInputBuffer.put(queuingBuffer.array(), 0, bufDataSize)
    codec.queueInputBuffer(
        codecBufId, 0,
        bufDataSize, 0, 0
    )

    // デコーダのバッファにコピーしたので古いフレームはクリアしておく
    codecInputBuffer.clear()
    queuingBuffer.clear()
}

Surfaceが更新される

コールバックのところに書いたようにフレームデータがデコードされ、出力準備が完了すると
onOutputBufferAvailableが呼ばれる。
releaseOutputBufferで第二引数をtrueにしてSurfaceを更新させる。

結果

480x360@30fps
Iフレームは1枚/秒
で設定したもの。

つぎ

次は受信側のタッチイベントを送信してなんちゃって遠隔操作をしたいと思います。

コード

送信側(Encode)
受信側(Decode)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む