- 投稿日:2020-05-31T23:59:47+09:00
Spek(Android)入門
はじめに
AndroidのUnitTestでSpekが流行しているので(今更ですが)、導入からはじめてみました。
この記事では、公式ドキュメントを元にAndroidプロジェクトのセットアップから
UnitTestの実行までを記載します。そもそも、Spekとは?何が便利か?についてはこのあたりの記事やDroidKaigiでもよく紹介されているので、
そちらを閲覧してみてください。
https://qiita.com/k_keisuke/items/815ced486e8cdff8670d
https://qiita.com/sadashi/items/a5b93c9fc06e084456d2導入
1.JUnit5を実行するため、サードパーティのライブラリを導入。
プロジェクト自体はAndroidStudio標準のFragment + ViewModel で作成しました。
https://github.com/mannodermaus/android-junit5 のページに記載されている
gradleの設定をします。2.Android modules の設定
spek_version の変数やその他の設定はgradle.propertiesに記載します。
(うまく行かない部分は 公式のandroid sampleを参考にしました。 )ここまで行けばSpekのビルドが通ります。
3.SpekのUnitTest
最終形はこのように書いてみました。
object ExampleSpekUnitTest: Spek({ describe("A Calculator") { val calculator by memoized { Calculator() } it("should return 4") { assertEquals(4, calculator.add(2, 2)) } it("should return 10") { assertEquals(10, calculator.add(5, 5)) } it("should return 12") { assertEquals(12, calculator.add(6, 6)) } } describe("view model verify") { val viewModel by memoized { MainViewModel() } it("should return text MainViewModel") { assertEquals("MainViewModel", viewModel.getText()) } } })Spekは
gradlew test
でも実行できますが、
https://www.spekframework.org/running/ に記載されているPluginを導入すると
JUnitと同様に右クリックで実行できるので便利です。
実行後
実行が階層形式で表現されたり、実行順序が維持されることが見えていい感じです!
まとめ
一度Spekの導入を行ったときに導入に手間取ったので、復習や今後を含めて記載してみました。
仕様のように記載できるテストはよりテストの価値が生まれるので、今後はどんどん使ってみます!
- 投稿日:2020-05-31T22:48:58+09:00
M5StickCのボタン押下でスマホから現在地をしゃべらせる(1/2)
以前の投稿 GoogleMapを使ってサイクリングに出かけよう で、GoogleMapに音声ナビゲーションしてもらいました。
今回は、GoogleMapに頼らずに、スマホのGPSだけでナビゲートしてもらいます。
ですが、自転車運転中は、スマホの画面を見るのは危険ですので、やっぱり音声でナビゲートして欲しいところです。
例えば、「目的地まで100メートル、北西方向です。」と言ってくれれば、助かります。方向はどうやって認識するかというと、そこはアナログに方位磁針に助けを借りて、自転車に取り付けました。自転車ベルと方位磁針がセットのやつが700円弱です。ずっと、音声でだらだらと音声ナビしてもらうのもよいのですが、できれば、教えてほしい時に音声ナビして欲しいです。そこで、M5Stick-Cの出番です。バッテリが付いて単独で動き、BLEとボタンがついているので、無線でボタン押下をスマホに伝えることができそうです。
あとは、Androidアプリの開発だけです。いくつかの重要なAndroidの機能を使います。
- バックグラウンドで現在地取得と、BLE接続の維持が必要です。しかも、別のアプリが起動していたり、画面をOffにした状態でも動いている状態にしておく必要があります。そのために、Androidの「フォアグラウンドサービス」および「NotificationManger」という機能を使います。
- 位置情報の取得には、Androidの「LocationManager」を使います。
- 音声合成には、Androidの「TextToSpeech」の機能を使います。
- M5StickCとの接続には、「BLEセントラル」の機能を使います。
順番としては以下の通りです。
まずは準備から。
- Androidスマホで、フォアグラウンドサービスを起動します。
- NotificationManagerを使って常時通知をし、フォアグラウンドサービス稼働状態にします。
- LocationManagerを使って、位置情報の定期的な取得を開始します。
- BLEセントラルを使って、M5StickCをスキャンし、接続状態にします。
BLE接続されたM5StickCでボタンが押下されると、
- M5StickCからBLEのNotificationを受信します。
- HTTP Postを使って目的地の位置情報(緯度経度)を取得します。
- 現在地情報と目的地の位置情報を使って、距離と方位を計算します。
- TextToSpeechを使って、「目的地まで100メートル、方位は北西です」と音声再生します。
投稿第二弾はこちらです。Arduino編です。
M5StickCのボタン押下でスマホから現在地をしゃべらせる(2/2)以降で、それぞれのAndroidの機能を順番に説明します。
フォアグラウンドサービスの起動
こちらを参考にしています。
https://developer.android.com/about/versions/oreo/background-location-limits?hl=ja
https://akira-watson.com/android/gps-background.htmlServiceを継承したクラスを用意します。
LocationService.javapublic class LocationService extends Service{フォアグラウンドサービスとして起動を受けられるように、AndroidManifest.xmlに以下を記載します。
AndroidManifest.xml<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> ・・・ <service android:name=".LocationService" android:parentActivityName=".MainActivity" />最初のActivityから以下のように起動させます。
MainActivity.javaIntent intent = new Intent(getApplication(), LocationService.class); startForegroundService(intent);ちなみに、上記はAndroid8.0以降が前提です。
Service側では、以下の2つが呼び出されます。
LocationService.java@Override public void onCreate() { super.onCreate(); ・・・ @Override public int onStartCommand(Intent intent, int flags, int startId) {前者でNotificationの準備
LocationManager.javanotificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); if( notificationManager == null ) { Log.d(MainActivity.TAG, "NotificationManager not available"); return; }特に後者の中で、5秒以内に、常時通知(Notification)を作る必要があるそうです。
例えば、こんな感じです。LocationService.javaif( notificationManager != null ){ Intent notifyIntent = new Intent(this, ResultActivity.class); notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT ); NotificationChannel channel = new NotificationChannel(CHANNEL_ID, NOTIFICATION_TITLE , NotificationManager.IMPORTANCE_DEFAULT); // 通知音を消さないと毎回通知音が出てしまう // この辺りの設定はcleanにしてから変更 channel.setSound(null,null); channel.enableLights(false); channel.setLightColor(Color.BLUE); channel.enableVibration(false); notificationManager.createNotificationChannel(channel); Notification notification = new Notification.Builder(context, CHANNEL_ID) .setContentTitle(NOTIFICATION_TITLE) .setSmallIcon(android.R.drawable.btn_star) .setContentText("GPSとBLEでナビゲーション中") .setAutoCancel(true) .setContentIntent(pendingIntent) .setWhen(System.currentTimeMillis()) .build(); startForeground(NOTIFICATION_ID, notification); }notificationを作成して、startForegroundを呼び出しています。
また、作成した通知が表示され、それをタッチすることでアクティビティを起動するようにしています。PendingIntentのところです。
起動には標準のアクティビティと特殊なアクティビティの2種類の方法があるそうです。
https://developer.android.com/training/notify-user/navigation?hl=ja今回は、通知からの起動専用のアクティビティを用意しようと思うので、特殊なアクティビティを採用しています。
位置情報の取得
位置情報の適宜の取得にLocationManagerを使っています。
そのために、AndroidManifest.xmlに以下を追加します。AndroidManifest.xml<uses-permission android:name="android.permission.ACCESS_GPS" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />appフォルダの下にあるbuild.gradleに以下を追記します。
build.gradleimplementation 'com.google.android.gms:play-services-location:17.0.0'まずは、スマホ操作者に対して、GPSを使った位置情報の取得の許可をもらいます。
MainActivity.javaLocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); final boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); if (!gpsEnabled) { Intent settingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); startActivity(settingsIntent); } checkLocationPermissions(); ・・・ private void checkLocationPermissions(){ 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 { isLocationReady = true; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_PERMISSION_LOCATION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { isLocationReady = true; }else{ Toast.makeText(this, "位置情報の許可がないので計測できません", Toast.LENGTH_LONG).show(); } } }あとは、LocationService.javaの中で、準備として、
LocationService.javalocationManager = (LocationManager) getSystemService(LOCATION_SERVICE); if( locationManager == null ) { Log.d(MainActivity.TAG, "LocationManager not available"); return; }ののち、以下を呼び出します。
LocationService.javaprivate void startGPS() { if (locationManager != null) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) return; locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, MinTime, MinDistance, locationListener = new LocationListener() { @Override public void onLocationChanged(Location location) { Log.d(MainActivity.TAG, "onLocationChanged"); CheckPoints.location = location; } @Override public void onStatusChanged(String s, int i, Bundle bundle) { } @Override public void onProviderEnabled(String s) { } @Override public void onProviderDisabled(String s) { } }); } }MinTimeと、MinDistanceは、現在地情報の更新の通知の頻度を調整するためのものです。主に、電池の消費を抑えるためのものです。
前者は例えば、5秒間は連続した通知はしない。後者は例えば、25メートル以上の前回からの位置差分がなければ通知しない。といったように、抑制します。あとは、上記条件が満たすと、onLocationChangedが呼ばれ、現在地locationが取得できるようになります。
TextToSpeechを準備
まずは準備
LocationService.javatts = new TextToSpeech(this, new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { if( status == TextToSpeech.SUCCESS ) { isTtsReady = true; }else{ Log.d(MainActivity.TAG, "TextToSpeech init error"); } } });あとは、音声再生したい時に、以下を呼び出すだけです。
LocationService.javavoid doSpeech(String message){ if(tts != null && isTtsReady) tts.speak(message, TextToSpeech.QUEUE_ADD, null, TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID ); }BLEセントラル機能
まずは準備
AndroidManifest.xmlに以下を追記します。
AndroidManifest.xml<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />LocationService.javaの onCreateで以下の準備をしておきます。
LocationService.javabtManager= (BluetoothManager)context.getSystemService(Activity.BLUETOOTH_SERVICE); if( btManager == null ) { Log.d(MainActivity.TAG, "BluetoothManager not available"); return; } btAdapter = btManager.getAdapter(); if (btAdapter == null) { Log.d(MainActivity.TAG, "BluetoothAdapter not available"); return; }そして、2つのコールバックを用意します。
private final ScanCallback mScanCallback private final BluetoothGattCallback mGattcallback前者が、BLEスキャンで発見したデバイスを取得するためのもの。
後者で、BLE接続・切断イベント、BLEサービス検索結果のイベント、キャクタリスティックのRead/Write/通知/MTU変更 などなど、BLE通信で必要な処理を実装するコールバック関数を定義できますLocationService.javaprivate final ScanCallback mScanCallback = new ScanCallback(){ @Override public void onScanResult(int callbackType, ScanResult result) { Log.d(MainActivity.TAG, "onScanResult"); BluetoothDevice device = result.getDevice(); if( device == null ) return; String name = device.getName(); if( name != null && name.equals(WIRELESS_BLE_DEVICE_NAME) ){ if( mScanner != null ) { mScanner.stopScan(this); Log.d(MainActivity.TAG, WIRELESS_BLE_DEVICE_NAME + "を発見しました"); mConnGatt = device.connectGatt(context, false, mGattcallback); } } } };BLEペリフェラルを発見したら、デバイス名を調べて、所望のものかを判別しています。所望のものだったら、connectGattを呼び出して接続を試みます。
そして、さきほどのコールバックともに以下を呼び出すことで、BLEペリフェラルのスキャンが開始されます。LocationService.javaprivate void startBleScan(){ if( btAdapter != null && mScanner == null ) { mScanner = btAdapter.getBluetoothLeScanner(); mScanner.startScan(mScanCallback); } }もう一つのコールバック。
LocationService.javaprivate final BluetoothGattCallback mGattcallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { Log.d(MainActivity.TAG, WIRELESS_BLE_DEVICE_NAME + "と接続しました"); if (mConnGatt != null) mConnGatt.discoverServices(); }else if( newState == BluetoothProfile.STATE_DISCONNECTED ){ Log.d(MainActivity.TAG, WIRELESS_BLE_DEVICE_NAME + "と切断しました"); mConnGatt = null; if(mScanner != null) mScanner.startScan(mScanCallback); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { Log.d(MainActivity.TAG, "onServicesDiscovered"); BluetoothGattDescriptor descriptor; boolean result; mGattService = mConnGatt.getService( serviceUuid ); charNote = mGattService.getCharacteristic(noteUuid); result = mConnGatt.setCharacteristicNotification(charNote, true); if( !result ) { Log.d(MainActivity.TAG, "setCharacteristicNotification error"); return; } descriptor = charNote.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIG); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); result = mConnGatt.writeDescriptor(descriptor); if( !result ) { Log.d(MainActivity.TAG, "writeDescriptor error"); return; } charSend = mGattService.getCharacteristic(sendUuid); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { Log.d(MainActivity.TAG, "onCharacteristicChange"); if( characteristic.getUuid().compareTo(noteUuid) == 0) processIndication(characteristic.getValue()); }BLEペリフェラルとの接続が完了すると、onConnectionStateChangeが呼ばれます。その中で、BLEペリフェラルのサービスの検索のために、discoverServicesを呼び出しています。
さらに、プライマリサービスを検出すると、onServicesDiscovered が呼び出されます。その中では、Notificationを受けるために、Notificationを有効化したり、(必須ではありませんが、) 今後BLEセントラル側からBLEペリフェラル側にキャラクタリスティックWriteするために、UUIDを指定してキャラクタリスティックを取得しておきます。
BLEペリフェラルからのNotificationは、onCharacteristicChanged で受け取ります。
後始末
フォアグラウンドサービスが終了するときには、確保したBLE、TextToSpeech、LocationManagerをクローズしましょう。
終了時には、onDestroyが呼び出されますので、その中で後始末をします。LocationService.java@Override public void onDestroy() { Log.d(MainActivity.TAG, "onDestroy"); if( mConnGatt != null ){ mScanner = null; try { mConnGatt.disconnect(); mConnGatt.close(); }catch (Exception ex){} mConnGatt = null; } if(tts != null) { try { tts.stop(); tts.shutdown(); }catch (Exception ex){} tts = null; } if (locationListener != null) { locationManager.removeUpdates(locationListener); locationListener = null; } }おわりに
結構長い投稿となってしまいました。
いったんここで区切り、後は後半の投稿にします。
次回は、M5StickC側の実装です。Arduinoを使っています。また、ソースコード一式もGitHubに上げる予定です。以上
- 投稿日:2020-05-31T16:25:35+09:00
UnityからAndroid端末にアプリをインストールするときにでたエラーについて
エラー
adb: failed to install /Users/UserName/UnityProject/TestUnityProject/ProjectName/Builds/Apk/AppName.apk: Failure [INSTALL_FAILED_OLDER_SDK: Failed parse during installPackageLI: /data/app/vmdl1509473597.tmp/base.apk (at Binary XML file line #8): Requires newer sdk version #28 (current version is #27)]
```日本語訳
adb:/Users/UserName/UnityProject/TestUnityProject/ProjectName/Builds/Apk/AppName.apk:のインストールに失敗しました:[INSTALL_FAILED_OLDER_SDK:installPackageLIの解析に失敗しました:/data/app/vmdl1509473597.tmp/base.apk(バイナリXMLファイル)行#8):新しいSDKバージョン#28が必要(現在のバージョンは#27)]
解決策
「インストール先のOSのバージョン」と「Unityで吐き出すApiLevel」を合わせる
SDKがおかしいのかと思い、アップデートなどをやりまくったが、解決せず、、
エラー文を検索したら、こちらの記事が出てきて、バージョンが合ってないということがわかりました。
- 投稿日:2020-05-31T11:44:27+09:00
2020年6月から11月までに行われる開発者向け大規模カンファレンス・サミットをまとめみた!
こんにちは(๑╹ω╹๑ )
毎年夏場前後で開発者向けの大規模なカンファレンスやサミットが開催されていますよね!!今年は例年と違いオンラインでの参加が可能なイベントが増えております!
※この記事で紹介するイベントはオンラインでの視聴が可能なイベントのみですこのような記事を書き残そうと思ったのは、
個人的に情報の整理が追いついていなかったためです笑※日付は現地時間です
主催 イベント名 日程 備考 Microsoft Cloud Summit 6月1日〜6月5日 Azure、ブロックチェーン、AI、Dynamics 365、Power Platform Cisco Cisco Live 6月2日〜6月3日 Android Android116月3日 中止になりました Google Cloud Day 6月9日〜6月11日 アプリケーション、データ基盤・分析、コラボレーション、インフラストラクチャ、機械学習、セキュリティ Apple WWDC2020 6月22日 例年通り開発中のOS発表、Xcode11 SwiftUI、ARKit、Core ML 3 VMware VMworld 2020 6月23日 インフラストラクチャ、マルチクラウド、セキュリティ web.dev LIVE 6月30日〜7月2日 Chromiumを主軸としたWeb Next Moz Moz Con 7月14日〜7月15日 SEO、マーケティング担当者向け Google Cloud Next 7月14日から毎週(計9回 上記Google Cloud Dayと同等内容のセッション Microsoft Microsoft Ignite 9月(日時は未定) Azureを主軸としたインフラストラクチャ Adobe Adobe Max 10月19日〜10月21日 Adobe Creative Cloud 記載漏れ等あれば教えて下さい?
- 投稿日:2020-05-31T11:44:27+09:00
2020年6月から11月までに行われる開発者向け大規模オンラインカンファレンス・サミットをまとめみた!
こんにちは(๑╹ω╹๑ )
毎年夏季前後で開発者向けの大規模なカンファレンスやサミットが開催されていますよね!!今年は例年と違いオンラインでの参加が可能なイベントが増えております!
※この記事で紹介するイベントはオンラインでの視聴が可能なイベントのみですこのような記事を書き残そうと思ったのは、
情報の整理が追いついていなかったためです笑※日付は現地時間です
主催 イベント名 日程 備考 Microsoft Cloud Summit 6月1日〜6月5日 Azure、ブロックチェーン、AI、Dynamics 365、Power Platform Cisco Cisco Live 6月2日〜6月3日 Android Android116月3日 中止になりました Google Cloud Day 6月9日〜6月11日 アプリケーション、データ基盤・分析、コラボレーション、インフラストラクチャ、機械学習、セキュリティ Apple WWDC2020 6月22日 例年通り開発中のOS発表、Xcode11 SwiftUI、ARKit、Core ML 3 VMware VMworld 2020 6月23日 インフラストラクチャ、マルチクラウド、セキュリティ web.dev LIVE 6月30日〜7月2日 Chromiumを主軸としたWeb Next Moz Moz Con 7月14日〜7月15日 SEO、マーケティング担当者向け Google Cloud Next 7月14日から毎週(計9回 上記Google Cloud Dayと同等内容のセッション Microsoft Microsoft Ignite 9月(日時は未定) Azureを主軸としたインフラストラクチャ Adobe Adobe Max 10月19日〜10月21日 Adobe Creative Cloud 記載漏れ等あれば教えて下さい?