Androidをペンタブレットにする(2/3)

前回、[Androidをペンタブレットにする(1/3)]( https://qiita.com/poruruba/items/7a48fd52aa302bc48caa) を投稿しましたが、タッチパネルだけに飽き足らず、Androidスマホが持っているいろんな機能を取り込んでみました。

そのうち、以下の機能の実装を備忘録としておきます。スニペット的に残しておきましたので、コピペで使ってください。

•	Androidの地磁気センサー、ジャイロスコープ、加速度センサーを検出して、Windowsに通知する。
•	AndroidのGPS情報を取得し、Windowsに通知する。
•	AndroidでQRコードをスキャンし、Windowsに通知する。
•	Androidで音声認識して、Windowsに通知する。
•	AndroidのクリップボードにWindowsからコピーやペーストする。

# 地磁気センサー、ジャイロスコープ、加速度センサー

〇利用するクラス宣言

```java
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
```

〇変数定義

```java
  SensorManager sensorManager;
  boolean isGyroscope = false;
  boolean isAccelerometer = false;
  boolean isMagnetic = false;
```

〇onCreateにて

```java
    sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
```

センサー機能を有効、無効にします。
isMagnetic等は、アクティビティがアクティブでない場合に一時停止し、アクティブになったら再度開始にするためのものです。

```java
    private void setSensorEnable(int type, boolean enable){
        if( type == Sensor.TYPE_MAGNETIC_FIELD ) {
            isMagnetic = enable;
        }else if( type == Sensor.TYPE_GYROSCOPE){
            isGyroscope = enable;
        }else if( type == Sensor.TYPE_ACCELEROMETER ){
            isAccelerometer = enable;
        }
        if (enable)
            sensorStart(type);
        else
            sensorStop(type);
    }
```

アクティビティがアクティブでないときは、センサー検知を一時停止にします。

```java
    @Override
    protected void onResume(){
        super.onResume();

        if(isGyroscope)
            sensorStart(Sensor.TYPE_GYROSCOPE);
        if( isAccelerometer )
            sensorStart(Sensor.TYPE_ACCELEROMETER);
        if( isMagnetic )
            sensorStart(Sensor.TYPE_MAGNETIC_FIELD);
    }

    @Override
    protected void onPause(){
        super.onPause();

        sensorStop(Sensor.TYPE_GYROSCOPE);
        sensorStop(Sensor.TYPE_ACCELEROMETER);
        sensorStop(Sensor.TYPE_MAGNETIC_FIELD);
    }
```

以下が、センサー検知イベントの開始です。
registerListenerで登録したクラスにイベントが通知されます。SensorEventListenerクラスを実装する必要があります。イベントの頻度は、SENSOR_DELAY_NORMAL以外にもいくつか種類があります。

```java
    private void sensorStart(int type){
        if( sensorManager == null )
            return;

        if( type == Sensor.TYPE_MAGNETIC_FIELD ) {
            Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
            if (sensor != null) {
                sensorManager.registerListener(sensorListnerMagnetic, sensor, SensorManager.SENSOR_DELAY_NORMAL);
            }
        }else if( type == Sensor.TYPE_GYROSCOPE ){
            Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
            if (sensor != null) {
                sensorManager.registerListener( sensorListnerGyro,  sensor, SensorManager.SENSOR_DELAY_NORMAL);
            }
        }else if( type == Sensor.TYPE_ACCELEROMETER){
            Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
            if (sensor != null) {
                sensorManager.registerListener( sensorListnerAccel,  sensor, SensorManager.SENSOR_DELAY_NORMAL);
            }
        }
    }
```

以下が、センサー検知イベントの停止です。

```java
    private void sensorStop(int type){
        if( sensorManager == null )
            return;

        if( type == Sensor.TYPE_MAGNETIC_FIELD ) {
            sensorManager.unregisterListener(sensorListnerMagnetic);
        }else if( type == Sensor.TYPE_GYROSCOPE ){
            sensorManager.unregisterListener(sensorListnerGyro);
        }else if( type == Sensor.TYPE_ACCELEROMETER){
            sensorManager.unregisterListener(sensorListnerAccel);
        }
    }
```

以降、センサー検知イベントの受信処理のためのクラス実装です。地磁気センサー、ジャイロスコープ、加速度センサーとありますが、同じフォーマットです。

```java
    private SensorEventListener sensorListnerMagnetic = new SensorEventListener(){
        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {
            switch( sensorEvent.sensor.getType() ){
                case Sensor.TYPE_MAGNETIC_FIELD:{
// BLEセントラルにイベントを転送
                    int unit = Float.SIZE / Byte.SIZE;
                    byte[] send_buffer = new byte[1 + 3 * unit];
                    send_buffer[0] = Const.RSP_MAGNETIC;
                    setFloatBytes(send_buffer, 1, sensorEvent.values[0]);
                    setFloatBytes(send_buffer, 1 + unit, sensorEvent.values[1]);
                    setFloatBytes(send_buffer, 1 + 2 * unit, sensorEvent.values[2]);
                    sendBuffer(send_buffer, send_buffer.length);                    break;
                }
            }
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {
            // do nothieng
        }
    };

    private SensorEventListener sensorListnerGyro = new SensorEventListener(){
        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {
            switch( sensorEvent.sensor.getType() ){
                case Sensor.TYPE_GYROSCOPE:{
// BLEセントラルにイベントを転送
                    break;
                }
            }
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {
            // do nothieng
        }
    };

    private SensorEventListener sensorListnerAccel = new SensorEventListener(){
        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {
            switch( sensorEvent.sensor.getType() ){
                case Sensor.TYPE_ACCELEROMETER:{
// BLEセントラルにイベントを転送
                    break;
                }
            }
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {
            // do nothieng
        }
    };
```

#GPS情報

appのbuild.gradleのdependenciesに以下を追加します。

```gradel:build.gradle
    implementation 'com.google.android.gms:play-services-location:17.0.0'
```

AndroidManifest.xmlに以下を追加します。

```xml:AndroidManifest.xml
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
```

〇利用するクラス宣言

```java
import android.location.Location;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
```

〇変数宣言

```java
    FusedLocationProviderClient fusedLocationClient;
```

〇onCreateにて

```java
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
        LocationRequest locationRequest = new LocationRequest();
        locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
```

GPS情報取得には、ユーザに対して許可を得る必要があります。許可を得ていないのであれば、許可を要求し、すでに得ているのであれば、GPS情報を取得します。

```java
    private void getLocation() {
        if (fusedLocationClient == null)
            return;

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_PERMISSION_LOCATION);
        }else {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
                fusedLocationClient.getLastLocation().addOnCompleteListener(this, new OnCompleteListener<Location>() {
                    @Override
                    public void onComplete(@NonNull Task<Location> task) {
                        Location location = task.getResult();
                        sendLocation(location.getLatitude(), location.getLongitude());
                    }
                });
            }
        }
    }
```

GPS情報取得の許可をユーザから得ると、以下のActivityの関数が呼ばれます。許可を得たのち、GPSを取得します。

```java
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
        if( requestCode == REQUEST_PERMISSION_LOCATION )
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
                getLocation();
    }
```

# QRコードスキャン

こちらを使わせていただきました。

journeyapps/zxing-android-embedded
　https://github.com/journeyapps/zxing-android-embedded

appのbuild.gradleのdependenciesに以下を追加します。

```gradel:build.gradle
    implementation 'com.journeyapps:zxing-android-embedded:3.2.0@aar'
    implementation 'com.google.zxing:core:3.2.1'
```

〇利用するクラス宣言

```java
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
```

あとは、以下を呼ぶだけです。カメラが起動し、QRコードをスキャンします。

```java
    private void startQrScan(){
        new IntentIntegrator(this).initiateScan();
    }
```

スキャンが完了すると、以下のActivityの関数が呼ばれます。result.getContents()にスキャンした文字列があります。

```java
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
        if(result != null) {
            if(result.getContents() != null)
                sendQrcode(result.getContents());
            return;
        }

        super.onActivityResult(requestCode, resultCode, data);

// ・・・
```

# 音声認識

AndroidManifest.xmlに以下を追加します。

```xml:AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
```

〇利用するクラス宣言

```java
import android.speech.RecognizerIntent;
```

あとは、以下を呼ぶだけです。

```java
    private void doRecognize(){
        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.JAPAN.toString() );
        intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 100);
        intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "音声を入力");
        startActivityForResult(intent, REQUEST_RECOGNITION);
    }
```

音声認識が完了すると、Activityの以下の関数が呼ばれます。candidates.get(0)に一番可能性の高い文章が入っています。

```java
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

//・・・

        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == REQUEST_RECOGNITION && resultCode == RESULT_OK) {
            ArrayList<String> candidates = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
            if( candidates.size() > 0 )
                sendText(Const.TYPE_TEXT_RECOGNITION, candidates.get(0) );
        }
```

#クリップボード

〇利用するクラス宣言

```java
import android.content.ClipData;
import android.content.ClipboardManager;
```

〇変数宣言

```java
    ClipboardManager clipboardManager;
```

〇onCreateにて

```java
        clipboardManager = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
```

クリップボードから文字列を取得し、BLEセントラルに転送します。クリップボード操作には直接は関係しませんが、クリップボードから取得したことをToastでもわかるようにしました。

```java
    private void sendCopy(){
        if( clipboardManager == null )
            return;

        ClipData cd = clipboardManager.getPrimaryClip();
        if(cd != null){
            ClipData.Item item = cd.getItemAt(0);
            Toast.makeText(this, item.getText().toString(), Toast.LENGTH_LONG).show();
            sendText(Const.TYPE_TEXT_COPY, item.getText().toString());
        }
    }
```

BLEセントラルから受け取った文字列をクリップボードにコピーします。
クリップボード操作には直接は関係しませんが、クリップボードに受け取ったことをToast で表示したいのですが、UIスレッドでない場合に備えて、UIスレッドにメッセージ送信しています。

```java
    private void setPaste(String text){
        if( clipboardManager == null )
            return;

        clipboardManager.setPrimaryClip(ClipData.newPlainText("", text));
        handler.sendUIMessage(UIHandler.MSG_ID_TEXT, UIMSG_TOAST, "Receive Clipboard");
    }
```

以上