20200517のAndroidに関する記事は5件です。

AndroidStudioで辞書登録を共有する

はじめに

プロジェクト特有の言葉(日本語やビジネス用語)を使うときにAndroidStudioでは
丁寧に警告を出してくれるんですが、ミスリードなので警告を回避して、
チームで共有する方法を整理しました。

ちなみに、開発プロジェクトでスペルチェック用辞書を共有する方法
すごく丁寧に書いてくれています。この記事は主に自身の備忘録にあたります...

やったこと

1 typo対象外にしたいワードをAndroidStudioで辞書登録する
スクリーンショット 2020-05-17 19.37.56.png

2 辞書ファイルはOSユーザー名で作成されるため、プロジェクトで共有できるファイル名に変更する
スクリーンショット 2020-05-17 19.38.32.png

<dictionary name="os_user"> の name を変更するとファイル名自体も勝手に変更してくれるので便利です

3 (必要であれば).gitignoreを編集してGitで共有できるようにする

スクリーンショット 2020-05-17 19.53.44.png

プロジェクトで共有するファイルはgit登録、それ以外の辞書ファイルは無視する設定とかにしておくといいかもしれません。
あとは、GitにPushして終了です。

まとめ

コーディングに集中しているときは後回しにしがちなのでチームで開発するときの環境整備として
早めにできているといいと思いました。
プロジェクトで共有できるファイル名にしておくことで、個別の設定も各自でできるので有用かなと..
(個別の設定は必要に応じてマージしてもらわないといけないですが..)

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

AndroidのTwilio VideoViewの形をCircleにする

はじめに

あるプロジェクトでTwilioのVideoライブラリを使ったのですがTwilioのVideoViewはデフォルトでは、四角形でカメラ映像がレンダリングされます。
これをよくある丸形にするのが意外と苦戦したのでメモしておきます。

やりたいのはこういうやつ↓
スクリーンショット 2020-05-17 14.34.47.png

※ 今回の方法は下記のやり方を参考にしました。
https://github.com/twilio/video-quickstart-android/issues/291

やり方

  • 親のカスタムレイアウトを作成
CircleFrameLayout.kt
package com.xxxxx

import android.content.Context
import android.graphics.Canvas
import android.graphics.Path
import android.graphics.Region
import android.util.AttributeSet
import android.widget.RelativeLayout

open class CircleFrameLayout constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) {

    constructor(context: Context) : this(context, null, 0)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    private var circlePath = Path()

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        val radius = Math.min(w/2f, h/2f)

        circlePath.reset()
        circlePath.addCircle(w/2f, h/2f, radius, Path.Direction.CW)

        super.onSizeChanged(w, h, oldw, oldh)
    }

    override fun dispatchDraw(canvas: Canvas) {
        canvas.clipPath(circlePath, Region.Op.INTERSECT)
        super.dispatchDraw(canvas)
    }
} 
  • twilioはVideoViewではなくVideoTextureViewを使います。
    (VideoViewを使ってしまうと四角形の背景が残ったままになってしまうので注意)
activity_main.xml
        <com.xxxxx.CircleFrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/menu_top"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="@dimen/video_margin_small"
            android:layout_marginTop="@dimen/video_margin_small"
            >
            <com.twilio.video.VideoTextureView
                android:id="@+id/video_1"
                android:layout_width="@dimen/video_small"
                android:layout_height="@dimen/video_small"
                app:tviScaleType="fit"
                />
        </com.xxxxx.CircleFrameLayout>

これで最初に紹介したような丸形のViewに出来ます!

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

ExoPlayer Okhttp Extentionで ExoPlayerのネットワークをカスタマイズする

ExoPlayer Okhttp Extentionとは

ExoPlayerはAndroidで動画・音声を再生するフレームワークです。
ExoPlayerではたくさんの拡張ライブラリが用意されており、その中のひとつにOkhttp Extentionがあります。
Okhttp Extentionを使用すれば、OkHttpを使ってExoPlayerのネットワークをカスタマイズ出来ます。

具体的には、ExoPlayerが受け取るレスポンスデータを書きかえたり、ステータスコードを書き換えたり... Okhttpで出来ること全てが可能です
ただ、Httpタイムアウト時間・リクエストヘッダのカスタマイズ・ネットワークイベントなどはコアライブラリのみで対応可能です。

導入

gradleのdependencyにokhttp extentionを追加します。extentionのバージョンはExoPlayer本体のバージョンと一致させます。

def ver_exoplayer = 'x.x.x'
implementation "com.google.android.exoplayer:exoplayer-core:$ver_exoplayer"
implementation "com.google.android.exoplayer:extension-okhttp:$ver_exoplayer"

使い方

DefaultDataSource, DefaultDataSourceFactoryの代わりにOkHttpDataSource OkHttpDataSourceFactoryを用います。
例えば以下の例ではサーバー上の動画ファイルを再生・リクエスト時にログします。

val client = OkHttpClient().newBuilder().addNetworkInterceptor(object : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
             Log.d("LOG", "REQUEST INTERCEPTED")
             return chain.proceed(chain.request())
        }
}).build()
val okHttpDataSourceFactory = OkHttpDataSourceFactory(client, "USER_AGENT")
val mediaSource = ProgressiveMediaSource.Factory(okHttpDataSourceFactory)
        .createMediaSource("MP4_URL".toUri())
player.prepare(mediaSource)
player.playWhenReady = true

OkHttpDataSource/OkHttpDataSourceFactoryにはカスタマイズしたOkhttp.Callを渡すことができるので、この部分で自由にカスタマイズできるという訳です。

リクエストヘッダ・タイムアウト時間をカスタムする場合はOkhttp Extentionは不要

リクエストヘッダ・タイムアウト時間・リダイレクトなどの設定なら、コアライブラリに含まれるDefaultHttpDataSourceFactoryで対応可能です。
Okhttp Extentionを使用せずに済みます。

val factory = DefaultHttpDataSourceFactory(
  userAgent,
  connectTimeout,
  readTimeout, 
  allowCrossProtocolRedirects/*HTTP<=>HTTPS間のリダイレクトを許容するかどうか*/
)
factory.defaultRequestProperties.set("headerName", "headerValue")
val mediaSource = ProgressiveMediaSource.Factory(factory).createMediaSource("REMOTE_URL".toUri())
player.prepare(mediaSource)
player.playWhenReady = true

ネットワークイベントを作成する場合はOkhttp Extentionは不要

ダウンロードの進捗やリクエストの各パラメータを知りたいなら、コアライブラリのTransferListenerが便利です。
Okhttp Extentionを使用せずに済みます。

// dataSpecからリクエストパラメータやストリーム中のポジションを取得できます
// requestパラメータ:
//   dataSpec?.httpBody
//   dataSpec?.httpRequestHeaders
//   dataSpec?.httpMethod
// ストリーム中のポジション:
//   dataSpec?.position
//   dataSpec?.absoluteStreamPosition
val transferListener = object : TransferListener {
            override fun onTransferInitializing(
                source: DataSource?,
                dataSpec: DataSpec?,
                isNetwork: Boolean
            ) {

            }

            override fun onTransferStart(
                source: DataSource?,
                dataSpec: DataSpec?,
                isNetwork: Boolean
            ) {

            }

            override fun onTransferEnd(
                source: DataSource?,
                dataSpec: DataSpec?,
                isNetwork: Boolean
            ) {

            }

            override fun onBytesTransferred(
                source: DataSource?,
                dataSpec: DataSpec?,
                isNetwork: Boolean,
                bytesTransferred: Int
            ) {

            }
        }

val factory = DefaultDataSourceFactory(getApplication(), userAgent, transferListener)
val mediaSource = ProgressiveMediaSource.Factory(factory).createMediaSource("REMOTE_URL".toUri())
player.prepare(mediaSource)
player.playWhenReady = true

さいごに

Okhttp Extentionは自由度が高く何でもできますが、基本的なことならコアライブラリのみで実装可能です。
レスポンスデータそのもの書きかえたい時など、使用する場面は割と限られてくるかと思います。

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

モバイルアプリに対する難読化/堅牢化

クラッキングの現状

近年様々の分野においてアプリのクラッキング被害が多く報告されています。これは、初心者にも簡単に使えるクラッキングツールが、インターネットで入手可能となり、攻撃者の数を急増したためです。
体表的になクラッキング被害は以下のような例を挙げられます。
- ゲームチート
 - クラッキングにより不正ツールが作られ、課金回避や不正操作が横行します。
- 製造産業機器・ロボットのクローン
 - 組込アプリのアルゴリズムを解析され、クローン製品を作られます。
- 金融・フィンテック 電子決済のなりますし
 - アプリの改ざんにより、本人の認証の不正回避や、なりますしなどの原因となります。
- O2O 推薦者への報奨金のハッキング
 - O2Oアプリの特性上、機密にすべきユーザの個人情報をアプリ内で扱うことがあります。ハッカーは、アプ リケーションの動作を理解し、簡単な操作でエミュレータから新しいユーザとして認識される方式を利用し、不当な利益を請求してきました。

難読化/堅牢化とは

- 難読化
 - プログラムにおいて、その内部的な動作の手続き内容・構造・データなどを人間が理解しにくい、あるいはそのようにるよう加工されたソースコードやマシンコードのこと

- 堅牢化
 - 改竄を検知してアプリを停止させる
 - ロジック改竄検知、アプリ署名改竄検知、Root化/脱獄検知、デバッグモード検知、etc・・・

サービス紹介(Msafe Technologyのseciron)

クラッキング機能(一部)
スクリーンショット 0002-05-17 12.19.05.png

Msafe Technologyの堅牢化サービスはキャンペン実施しております。
ご希望のお客様はコーポレートサイト問い合わせフォームよりお問い合わせください。
Msafe Technology株式会社

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

AndroidとSesameとNFCで開けゴマ 最終版

動作結果

jobi9-m8t0q.gif

変更点

・Sesameの操作関係をクラス化
  インスタンスで2台操作できそうに(未実験
  10秒or1分で接続が切れることを利用して、それっぽく例外処理
・スマホとペアリングができていない場合は、createBondでペアリング
・マニフェストでActivityに対して”透明”テーマを適応
・Notificationでヘッドアップ通知で動作状況を把握
・BluetoothAdapterの初期化をdevelopersサイトのKotllin記載に修正

manufactreDataMacDataStringの見つけ方

このプログラムではPrimarySesameMacDataの値について
取得場所.png

BLEScannerのアドバタイズパケットの内容が見れる:RAWの部分の赤線部分。この資料の場合、[00-00-C1-02-02-02-02-02-02]になる。

成果物

MainActivity.kt

package jp.sakujira.opensesame

//App
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
//NFC
import android.nfc.NfcAdapter
//Bluetooth
import android.content.Context
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
//Notification
import android.os.Build
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat

//SetUp-Config
//NFCCardData
val CardID : String = -鍵にしたいCardID-

//Sesame UserData
val UserID : String  = -Sesameのメールアドレス-
val Password : String  = -公式APPから抜き出したパスワード-

//Primary Sesame
val PrimarySesameAddress : String = -SesameのBLEアドレスEx:FF:00:FF:00:FF:00-
val PrimarySesameMacData : String  = -Sesameのアドバタイズパケットの該当部分:0000C1020202020202-
val PrimarySesameLockMinAngle : Int = 10
val PrimarySesameLockMaxAngle : Int = 270

//Secondary Sesame
val SecondarySesameAddress : String = ""
val SecondarySesameMacData : String  = ""
val SecondarySesameLockMinAngle : Int = 10
val SecondarySesameLockMaxAngle : Int = 270

class MainActivity : AppCompatActivity() {
    //SesameDevice
    private lateinit var PrimarySesame : SesameDevice
    private lateinit var SecondarySesame : SesameDevice
    private val HasSecondary : Boolean = (SecondarySesameAddress.length > 0)

    //BLE用のアダプター作成
    private val BluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) {
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothManager.adapter
    }

    //NFC-Certification
    private var isNFCLoad : Boolean = false

    @ExperimentalUnsignedTypes
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //SesameDeviceの初期化
        PrimarySesame = SesameDevice(BluetoothAdapter.getRemoteDevice(PrimarySesameAddress), PrimarySesameMacData, PrimarySesameLockMinAngle, PrimarySesameLockMaxAngle, this)
        if(HasSecondary){
            SecondarySesame = SesameDevice(BluetoothAdapter.getRemoteDevice(SecondarySesameAddress), SecondarySesameMacData, SecondarySesameLockMinAngle, SecondarySesameLockMaxAngle,this)
        }

        //このアプリを開く[起因]はNFC情報を読み取り
        //A1.[起因]は、AndroidManifest.xmlに規定した<intent-filter>に起因する
        if(!isNFCLoad && NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
            //カードのID情報を取得
            var tagId: String = ""
            for (byte in intent.getByteArrayExtra(NfcAdapter.EXTRA_ID)) {
                tagId += String.format("%02X", byte) + ":"
            }
            //A2.読み込んだカードIDが一致すれば、BLE操作を開始
            println("Btest:" + tagId)
            if (tagId == CardID + ":") {
                Notificate("ちょっとまってね!")
                isNFCLoad = true
                PrimarySesame.ConnectGatt()
                if (HasSecondary) {
                    SecondarySesame.ConnectGatt()
                }
            }
        }
        finishAndRemoveTask()
    }

    //Sesameに対して操作を開始
    @ExperimentalUnsignedTypes
    fun StartToggle(){
        if(isNFCLoad){
            isNFCLoad = false
            PrimarySesame.PrimaryStart()
        }
    }

    //SesameDeviceからの結果を受信
    @ExperimentalUnsignedTypes
    fun SetCompleteState(mCommandState : Int, mLockState : Int){
        var _LockState = mLockState
        if(HasSecondary && mCommandState == 4){
            SecondarySesame.SecondayStart(mLockState)
            _LockState = 0
        }

        when(_LockState){
            1->Notificate("いってらっしゃい!")
            2->Notificate("おかえりなさい!")
        }
    }

    //通知処理
    fun Notificate(mContent:String){
        // Channelの取得と生成
        val channelId = "KeyToggle"

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //Android8 Oreo以上の場合
            //通知チャンネルを作り
            val channel = NotificationChannel(channelId, "Beacon", NotificationManager.IMPORTANCE_HIGH).apply {
                this.description = "Sesame施錠・解錠通知"
                this.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
                this.enableVibration(true)
            }

            // システムにチャンネルを登録する
            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(this,channelId).apply {
            setContentTitle("Sesame施錠・解錠通知")
            setContentText(mContent)
            priority = NotificationCompat.PRIORITY_DEFAULT
        }

        with(NotificationManagerCompat.from(this)){
            notify(1, notification.build())
        }
    }
}

SesameDevice.kt

package jp.sakujira.opensesame

import android.bluetooth.*
import java.security.MessageDigest
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class SesameDevice(val mDevice : BluetoothDevice, val mMadData :String, val mMinLockAngle : Int, val mMaxLockAngle : Int, val SesameService: MainActivity){
    //Gattへの操作
    private var mGatt: BluetoothGatt? = null

    //SesameがもつBLEのサービス検索詞
    private val ServiceOperationUuid : UUID          = UUID.fromString("00001523-1212-efde-1523-785feabcd123")
    private val CharacteristicCommandUuid : UUID     = UUID.fromString("00001524-1212-efde-1523-785feabcd123")
    private val CharacteristicStatusUuid : UUID      = UUID.fromString("00001526-1212-efde-1523-785feabcd123")
    private val CharacteristicAngleStatusUuid : UUID = UUID.fromString("00001525-1212-efde-1523-785feabcd123")

    //サービス検索結果:各サービスへの接続詞
    var CharStatus: BluetoothGattCharacteristic? = null
    var CharCmd:BluetoothGattCharacteristic? = null
    var CharAngle:BluetoothGattCharacteristic? = null

    //状態管理用の変数
    var CommandState : Int = 0          //次のコマンドを何を投げるかを管理
    var SesameState : Int  = 0         //Sesame側のカウンターを管理
    var LockState : Int    = 0         //開けるか・閉めるか・状態を聞くかを管理
    var ConnectState : Int = 0 //Gatt接続状況を管理

    //送信データ管理
    var mSendData: ArrayList<ByteArray> = ArrayList()
    var mSendPointer : Int = 0

    //Gattの接続を試みる
    @ExperimentalUnsignedTypes
    fun ConnectGatt(){
        when(ConnectState){
            0->{//未接続状態
                ConnectState = 1//接続テスト中へ変更
                mGatt = mDevice.connectGatt(SesameService, false, mGattcallback)
            }
            2->{//接続状態
                ConnectedGatt()
            }
        }
    }

    //Gattの接続の成功
    @ExperimentalUnsignedTypes
    fun ConnectedGatt(){
        SesameService.StartToggle()
    }

    //Gattの接続終了処理
    fun DisConnectGatt(){
        CharStatus = null
        CharCmd    = null
        CharAngle  = null
        ConnectState = 0
        SesameState  = 0
        LockState    = 0
        ConnectState = 0
    }

    //ここから処理順番管理
    @ExperimentalUnsignedTypes
    //For PrimaryDevice
    fun PrimaryStart(){
        StateCommand()
    }

    //For SecondayDevice
    @ExperimentalUnsignedTypes
    fun SecondayStart(mLockState : Int){
        LockState = mLockState
        ConnectState = 10
        StateCommand()
    }

    //次処理実行
    @ExperimentalUnsignedTypes
    fun NextState(){
        CommandState += 1

        //変更点:別スレッドで次のコマンドを実行
        GlobalScope.launch{
            StateCommand()
        }
    }

    //処理順番管理
    @ExperimentalUnsignedTypes
    fun StateCommand(){
        println("Btest:Start-" + CommandState)
        when (CommandState){
            //For PrimaryDevice
            0->GetSesameStatus() //B0.Sesameの状態取得:Sesameカウントを取得
            1->SendStartData()   //B1.LockState:0を送信しAngleを検知させる
            2->GetSesameAngle()  //B2.角度を取得し、施錠・解錠を取得
            3->SendStartData()   //B3.施錠・解錠コマンドを送信
            4->ControlComplete() //B4.ループを終えてアプリを閉じる

            //For SecondaryDevice
            10->GetSesameStatus() //B10.Sesameの状態取得:Sesameカウントを取得
            11->SendStartData()   //B11.施錠・解錠コマンドを送信
            12->ControlComplete() //B12.ループを終えてアプリを閉じる
        }
        println("Btest:End-" + CommandState)
    }
//ここまで処理順番管理

    //ここから通信開始関数
    //Sesameからステータスを取得-開始
    fun GetSesameStatus(){
        println("Btest:GetStatus")
        mGatt!!.readCharacteristic(CharStatus)
    }

    //Sesameに対してデータを送信-開始
    @ExperimentalUnsignedTypes
    fun SendStartData(){
        println("Btest:SendData:")
        //各パラメータから送信データを作成
        val PayLoad = CreateSign(LockState,"", Password, mMadData, UserID, SesameState)
        //データをmtu:20byteごとに分割
        mSendData = SplitByteArray(PayLoad)
        mSendPointer = 0
        //送信データをセット
        println("Btest:SendData:Pointer"+ mSendPointer)
        println("Btest:SendData:" + ByteArrayToString(mSendData[mSendPointer]))
        CharCmd!!.setValue(mSendData[mSendPointer])
        mGatt!!.writeCharacteristic(CharCmd)
    }

    //Sesameから角度情報取得-開始
    fun GetSesameAngle(){
        println("Btest:GetRange")
        mGatt!!.readCharacteristic(CharAngle)
    }

    //Sesameへの操作処理終了処理
    @ExperimentalUnsignedTypes
    fun ControlComplete(){
        println("Btest:EndConnect!")
        //BLEのペアリングを保存
        if(mDevice.bondState == BluetoothDevice.BOND_NONE) {
            println("Btest:BOND-NONE")
            if(mDevice.createBond()){
                println("Btest:BOND-Create")
            }
        }
        //Serviceに状態を報告
        SesameService.SetCompleteState(CommandState,LockState)
        mGatt?.disconnect()
    }
//ここまで通信開始関数

    //ここから通信応答処理
    @ExperimentalUnsignedTypes
    private val mGattcallback: BluetoothGattCallback = object : BluetoothGattCallback() {
        //B0.Sesameへの接続確認
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)
            //B0.接続確立を確認して
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                //B0.サービスの検索を開始
                println("Btest:GattSa-Search")
                gatt?.discoverServices()
            }else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                DisConnectGatt()
            }
        }

        //B0.サービスの検索完了:結果を分析
        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            super.onServicesDiscovered(gatt, status)

            //B0.サービスの検索が成功を確認して
            if(status == BluetoothGatt.GATT_SUCCESS) {
                println("Btest:GattSa-OK!")
                //B0.サービスの一覧表を取得
                val GattSList: List<BluetoothGattService> = gatt?.services as List<BluetoothGattService>

                //B0.サービスの一覧表を走査
                for (GaService: BluetoothGattService in GattSList) {
                    println("Btest:>" + GaService.uuid.toString())

                    //B0.事前にserviceOperationUuidと一致したものがあったら、
                    if (GaService.uuid.equals(ServiceOperationUuid)) {
                        //B0.サービスが持っている機能・情報の一覧を取得
                        val GattCList: List<BluetoothGattCharacteristic> = GaService.characteristics
                        //B0.機能・情報の一覧の走査
                        for (GaCharacteristic: BluetoothGattCharacteristic in GattCList) {
                            println("Btest:>>" + GaCharacteristic.uuid.toString())
                            //B0.Sesameの状態情報取得の接続詞を取得
                            if (GaCharacteristic.uuid.equals(CharacteristicStatusUuid)){
                                CharStatus = GaCharacteristic
                            }
                            //B0.Sesameのコマンド情報取得の接続詞を取得
                            if (GaCharacteristic.uuid.equals(CharacteristicCommandUuid)){
                                CharCmd = GaCharacteristic
                            }
                            //B0.Sesameの角度情報取得の接続詞を取得
                            if (GaCharacteristic.uuid.equals(CharacteristicAngleStatusUuid)){
                                CharAngle = GaCharacteristic
                            }
                        }
                    }
                }
                //B0.走査した結果が全てあるかどうかをチェックし、次の状態に移行
                if(!(CharStatus == null || CharCmd == null || CharAngle == null)){
                    ConnectState = 2
                    ConnectedGatt()
                }
            }
        }

        //接続詞を使っての読み込み依頼した結果を分析
        override fun onCharacteristicRead(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
            super.onCharacteristicChanged(gatt, characteristic)

            println("Btest:" + characteristic!!.uuid.toString())
            when (characteristic.uuid) {
                //B2.依頼内容が「AngleStatus」であれば
                CharacteristicAngleStatusUuid -> {
                    val data: ByteArray = characteristic.value      //データを取得
                    println("Btest:" + ByteArrayToString(data))     //データをFF:FF形式で表示
                    val angleRaw = ByteArrayToInt(data.slice(2..3).toByteArray())   //データを切り出して、Byte→Intへ
                    val angle = Math.floor(angleRaw * 360 / 1024.0)      //角度を計算

                    //B2.LockStateは、操作コマンドと兼ねているため 1:解錠(操作:施錠) 2:施錠(操作:解錠)と逆になる
                    LockState = 1;
                    if (angle < mMinLockAngle || angle > mMaxLockAngle) {
                        LockState = 2;
                    }
                    println("Btest:Byte:" + ByteArrayToString(data) + ", Angle:" + angle + ", LockStatus:" + (LockState==2))
                    NextState()//次の状態に移行
                }
                //B0.依頼内容が「Status」であれば
                CharacteristicStatusUuid -> {
                    val data: ByteArray = characteristic.value          //データの取得
                    val Sn: Int = ByteArrayToInt(data.slice(6..9).toByteArray()) + 1    //Sesameカウンターを取得
                    val Err: Int = ByteArrayToInt(data.slice(14..14).toByteArray()) + 1 //エラーコードを取得
                    //エラーコードリスト
                    val errMsg = arrayOf(
                        "Timeout",
                        "Unsupported",
                        "Success",
                        "Operating",
                        "ErrorDeviceMac",
                        "ErrorUserId",
                        "ErrorNumber",
                        "ErrorSignature",
                        "ErrorLevel",
                        "ErrorPermission",
                        "ErrorLength",
                        "ErrorUnknownCmd",
                        "ErrorBusy",
                        "ErrorEncryption",
                        "ErrorFormat",
                        "ErrorBattery",
                        "ErrorNotSend"
                    )
                    println("Btest:Byte:" + ByteArrayToString(data) + ", Sn:" + Sn + ", Err:" + errMsg[Err])
                    SesameState = Sn    //B0.Sesameカウンタを記録
                    NextState()//次の状態に移行
                }
            }
        }

        //B1.B3.送信データの受領確認後、次パケットを送信
        override fun onCharacteristicWrite( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
            super.onCharacteristicWrite(gatt, characteristic, status)

            if(status == BluetoothGatt.GATT_SUCCESS){
                //送信完了したので、ポインタを一つ進める
                mSendPointer += 1
                //全部送信し終えたら
                if(mSendData.size <= mSendPointer){
                    println("Btest:SendData:Pointer-End")
                    //送信データを綺麗にしてから
                    mSendData = ArrayList()
                    mSendPointer = 0
                    //次の処理へ
                    NextState()
                }else {
                    println("Btest:SendData:Pointer"+ mSendPointer)
                    println("Btest:SendData:" + ByteArrayToString(mSendData[mSendPointer]))
                    //ここも別スレッドで残りを送信!
                    GlobalScope.launch {
                        CharCmd?.setValue(mSendData[mSendPointer])
                        gatt?.writeCharacteristic(CharCmd)
                    }
                }
            }
        }
    }
//ここまで通信応答処理

//ここからデータ処理関数
    //Sesameに対してデータを送信(BLEの20バイト制約のため、分割して送信)
    @ExperimentalUnsignedTypes
    fun SplitByteArray(pPayload : ByteArray): ArrayList<ByteArray>{
        val Data : ArrayList<ByteArray> = ArrayList()

        //送信は20バイトごとに送信
        //分割する際には、[先頭:01][中間:02][最後:04]と付ける必要がある
        //なので、一回の送信は19バイトごと
        for(i in 0..pPayload.size step 19){
            val wSz = Math.min(pPayload.size-i,19)      //送るデータが最後かどうか?
            var wCc : Int = 2           //初期値は[中間:02]とする
            var wBuf : ByteArray  = ByteArray(wSz+1)    //送信用データ場所を作成

            //先頭・最後を判定
            if(wSz < 19){
                wCc = 4
            }else if(i == 0){
                wCc = 1
            }

            //バイト列に分割詞をつける
            wBuf[0] = wCc.toByte()
            //送信データからバイト列を切り出し
            wBuf = ByteArrayCopy(wBuf, 1, pPayload, i,wSz)
            println("Btest:CutData:" + ByteArrayToString(wBuf))
            Data.add(wBuf)
        }
        return Data
    }

    //認証用バイトデータを作成(普段Byteを使わないから、符号あり・なしに振り回された、、、)
    fun CreateSign(pCode:Int, pPayload: String, pPassword : String, pMacData : String, pUserid : String, pNonce: Int) : ByteArray{
        //バイト配列の場所を作成
        var wBufnonPw : ByteArray = ByteArray(59 - 32 + pPayload.toByteArray().size)

        //manufactreDataのデータをコピー
        val ByteMacData : ByteArray = HexStringToByteArray(pMacData)
        println("Btest:Mac:" + ByteArrayToString(ByteMacData.sliceArray(3..ByteMacData.size-1)))
        wBufnonPw = ByteArrayCopy(wBufnonPw, 0, ByteMacData.sliceArray(3..ByteMacData.size-1),0,6)

        //md5のデータをコピー
        val md5 = MessageDigest.getInstance("MD5").digest(pUserid.toByteArray())
        println("Btest:md5:" + ByteArrayToString(md5))
        wBufnonPw = ByteArrayCopy(wBufnonPw,6,md5,0,16)

        //Status(Nonce)をコピー
        println("Btest:Nonce:" + ByteArrayToString(InttoByteArrayUnsign(pNonce)))
        wBufnonPw = ByteArrayCopy(wBufnonPw,22,InttoByteArrayUnsign(pNonce),0,4)

        //Codeをコピー
        println("Btest:Code:" + ByteArrayToString(InttoByteArrayUnsign(pCode)))
        wBufnonPw = ByteArrayCopy(wBufnonPw,26,InttoByteArrayUnsign(pCode),0,1)

        //Payloadをコピー
        wBufnonPw = ByteArrayCopy(wBufnonPw, 27, pPayload.toByteArray(),0, pPayload.toByteArray().size)

        //パラメータの結果を確認
        println("Btest:PrameterOK!:" + ByteArrayToString(wBufnonPw))

        //「生成したパラメータ」を「パスワード」を使って暗号化
        //「パスワード」を使用する暗号機を作成
        val key = SecretKeySpec(HexStringToByteArray(pPassword), "HmacSHA256")
        val mac = Mac.getInstance("HmacSHA256")
        mac.init(key)
        val wBufKey = mac.doFinal(wBufnonPw)
        println("Btest:wBufkey:" + ByteArrayToString(wBufKey))

        //全部を連結
        var wBuf : ByteArray = ByteArray(pPayload.toByteArray().size + 59)
        wBuf = ByteArrayCopy(wBuf,0, wBufKey,0, 32)
        wBuf = ByteArrayCopy(wBuf,32, wBufnonPw,0, wBufnonPw.size)
        println("Btest:ALL:" + ByteArrayToString(wBuf))

        return  wBuf
    }

    //Intを符号なしのバイト列に変換
    fun InttoByteArrayUnsign(num : Int): ByteArray{
        val wHexString : String = num.toString(16).padStart(12,'0')//文字埋めを12桁にしているのはByteArrayCopyで参照値外がないようにするため
        val wResult = HexStringToByteArray(wHexString)
        return  wResult.reversedArray() //1101→03F3となるが、送信データ上ではF303と逆にする必要がある
    }

    //HEX文字列をバイト配列にキャスト
    fun HexStringToByteArray(pHexString: String): ByteArray {
        val wBArray = ByteArray(pHexString.length / 2)
        for (index in 0 until wBArray.count()) {
            val pointer = index * 2
            wBArray[index] = pHexString.substring(pointer, pointer + 2).toInt(16).toByte()
        }
        return wBArray
    }

    //Byte配列を指定位置にコピー
    fun ByteArrayCopy(pTarget: ByteArray, pPosition: Int, pCopy: ByteArray, pStart: Int, pLength : Int):ByteArray{
        for(i in 0 until pLength){
            pTarget[pPosition + i] = pCopy[pStart + i]
        }
        return pTarget
    }

    //Byte配列を文字列化
    fun ByteArrayToString(pBytes: ByteArray): String{
        var wRsult : String = ""
        for (b in pBytes) {
            wRsult += String.format("%02X", b) + ":"
        }
        return wRsult
    }

    //Byte配列を数値化
    @ExperimentalUnsignedTypes
    fun ByteArrayToInt(pBytes: ByteArray): Int {
        var wResult: Int = 0
        for (b in pBytes.reversed()){
            wResult = wResult shl 8
            wResult += b.toUByte().toInt()
        }
        return wResult
    }
//ここまでデータ処理関数
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.sakujira.opensesame">

    <uses-permission android:name="android.permission.NFC" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:theme="@android:style/Theme.Translucent"><!--実行時にActivityが見えないように-->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
        </activity>
    </application>
</manifest>

kotlinx.coroutinesを使うので、buid.gradle(app)に以下を追加

dependencies {
・・・・
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:X.X.X'
}

参考資料

アドバタイズパケットの分析とBeacon検討

aBeacon ~iBeacon を Android で受信する~
https://gaprot.jp/2014/03/27/abeacon/

Android Beacon Library
https://altbeacon.github.io/android-beacon-library/index.html

[Android][iBeacon] Android Beacon Library パラっと解説 その4 [距離観測]
https://dev.classmethod.jp/articles/android-beacon-library-introduction-4/

Kotlinのサービス・レシーバーの書き方について

BLEMonitor
https://github.com/SergeiSafrigin/BLEMonitor/blob/master/src/kcg/ble/BleMonitorService.java

AndroidでServiceと通信する(Kotlinサンプル)
https://qiita.com/satken2/items/49dd76d848ceb208e937

コジオニルク-プロセスが同一の場合の例
https://kojion.com/posts/649

Notificationの書き方

OreoでNotificationを表示させる方法
https://qiita.com/naoi/items/367fc23e55292c50d459

Androidで(ヘッドアップ)通知を表示するサンプル
https://qiita.com/gpsnmeajp/items/33392aac8d00b55bce75

Activityを見せなくする方法

Theme.Translucentを継承せずにActivityの背景を透過にする
https://qiita.com/kgmyshin/items/a259f31b06ebab637044

考察:このアプリのセキュリティについて

単純にこのアプリは
 Sesame公式APP:ロック解除→アプリ起動→Sesame認識→ロック解除

 このアプリ:ロック解除→NFCかざす→ロック解除
としているので、公式アプリと同じ位の危険性と考えています。
自宅や職場のwifiでロック解除するアプリを使っていたとしても、同じくらい、多少待ち時間が少ない分危険性は高くなるかなと考えています。

仮に、NFCカード側を持っていかれても、Sesameに関する情報はなにもないので問題なし。
また、NFCカード、スマホ、スマホのロック解除が揃っているなら、公式アプリで解除するでしょうし、そもそも鍵を開けるためにはBLEの通信範囲にいないといけないので、「こんな事をしている」ことを知っている人に絞られるかなっと思ったり。

まぁ、Sesameを着けているいる時点である程度は家の鍵にバックドアの可能性が増えていると思っているので、まぁいいかなっと自分自身は考えています。
(まぁ、アプリの情報抜かれたらと考えるのであれば、そもそもこのアプリ自体が抜いているので、、、、)

編集後記

この話についてはココで区切り。
あとやるとしたら、施錠解錠の動画の撮影くらいかな~
最終版の書き方にして、かざす→施錠・解錠の時間が短くなったので、バックグランドサービスでSesameをScanLEで定期的に探そうかと思っていましたが、しなくていいかなっと感じています。
(その調査の結果、アドバタイズパケットからMacDataの位置が理解できたのですが、、、、

楽しかったけど、疲れた、、、。

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