コンテンツへスキップ

Android1.6端末(IS01)でBluetooth RFCOMMを使う方法

古いAndroid端末を活用するのに、Arduinoなどのマイコンボードと通信出来ればぐっと利用価値が高まります。

UARTが使えれば一番簡単だと思いますが、そんなものが無改造でつながる機種はレアでしょう。通信インタフェースとして使えそうでなものはUSB,Wifi,Bluetoothです。

この中でBluetoothのSPプロファイルではRFCOMMというシリアル通信用プロトコルが利用できます。これを使えば、接続さえしてしまえばUARTと同じ感覚で使えます。都合の良いことにマイコンと接続出来るBluetoothモジュール(技適つき)は比較的に安価で販売されていて、ArduinoにはBluetoothモジュールを使ったサンプルプログラムもあります。

まさにベストマッチと言いたいですが、Android1.6では通常はRFCOMMが使えない機種が多く、さらに独自実装であったりして一筋縄ではいきません(まさにIS01がこれです)。ここではAndroid1.6端末でRFCOMMを使う方法について記載したいと思います。

環境

今回の記事で使用した環境は以下の通りです。

Bluetooth SPPクライアント

  • SHARP IS01(Android 1.6)
  • 自作テストアプリ + backport-android-bluetoothライブラリ(IS01向けに修正)

Bluetooth SPPサーバ

  • Ubuntu 16.04(VirtualBox上に構築)
  • 自宅にあったBluetoothドングル(メーカ不明)
  • rfcomm + minicom

なお、記事で使用したテストプログラムは以下リポジトリにアップロードしてあります。参考になればと思います。

 

RFCOMMサーバを準備する

今回、Android端末とRFCOMM通信するためにPC上にテスト環境を用意しました。Android側をRFCOMMクライアントとして使うので、PC側はサーバとして接続を待ち受けることになります。

サーバ環境はVirtualBoxの仮想環境にLinux(Ubuntu)をインストールして構築しました。Linux環境ではBluetoothの通信内容を簡単にキャプチャー出来るのでデバッグに重宝しました。

下記の記事を参考にして頂ければと思います。

 

Android1.6ではBluetooth APIを叩くのにライブラリを使う

私の持っているIS01はAndroid1.6で、このバージョンはBluetooth APIが非公開です。android.bluetoothにアクセスできないため、通常は自作アプリケーションからBluetoothを操作することが出来ません。

しかし内部的にはBluetooth関連の機能が存在しているので、これにアクセスするためのライブラリがインターネット上に公開されています。このライブラリを使うことでアプリケーションからBluetoothをいじれるようになります(ライブラリ内部では非公開のAPIを呼び出すことで機能を実現しているようです)。

私が探した限りでは、ライブラリは以下の2種類がありました。
backport-android-bluetoothはAndroid公式APIと同じように使うことが出来るのでおすすめです。ただ、IS01はBluetoothの実装が他とは違うようでちょっとした改造が必要でした。

 

IS01で難儀する

この項の内容は、恐らくはSHARP IS01固有のものです。お使いの機種の実装次第なので断言は出来ませんが、IS01以外はこの項をすっ飛ばして素直にbackport-android-bluetoothライブラリを使えば動作するかと思います。

 

RFCOMMを使うと落ちる

意気揚々とライブラリを組み込んで動作させてみると、どうにも動作しません。両方のライブラリを試したのですが結果は同じです。BluetoothのON/OFFやペアリングしたデバイスの一覧は問題なく取得出来ているのですが、肝心のRFCOMMで接続しようとすると例外が発生して動作しません。

エラーログを見ると、どうもRfcommSocket.initNativeが無いと言っているように見えます。雲行きが怪しくなってきました。

android-bluetoothライブラリで接続を試みた際のエラー

09-08 09:07:29.957 2170-3998/com.suke_blog.androidbluetoothtest D/AndroidBluetoothTest: Connecting...
09-08 09:07:29.957 2170-3998/com.suke_blog.androidbluetoothtest D/LocalBluetoothDevice: creating new client BluetoothSocket for 00:15:83:15:A3:10 on port 3
09-08 09:07:29.967 2170-3998/com.suke_blog.androidbluetoothtest W/dalvikvm: No implementation found for native Landroid/bluetooth/RfcommSocket;.classInitNative ()V
09-08 09:07:29.967 2170-3998/com.suke_blog.androidbluetoothtest W/dalvikvm: Exception Ljava/lang/UnsatisfiedLinkError; thrown during Landroid/bluetooth/RfcommSocket;.<clinit>
09-08 09:07:29.967 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: it.gerdavax.android.bluetooth.BluetoothException: java.lang.ExceptionInInitializerError
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at it.gerdavax.android.bluetooth.LocalBluetoothDevice$BluetoothSocketImpl.init(LocalBluetoothDevice.java:543)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at it.gerdavax.android.bluetooth.LocalBluetoothDevice$BluetoothSocketImpl.connect(LocalBluetoothDevice.java:548)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at it.gerdavax.android.bluetooth.LocalBluetoothDevice$BluetoothSocketImpl.<init>(LocalBluetoothDevice.java:510)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at it.gerdavax.android.bluetooth.LocalBluetoothDevice$RemoteBluetoothDeviceImpl.openSocket(LocalBluetoothDevice.java:323)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at com.suke_blog.androidbluetoothtest.GPSSample$2.run(GPSSample.java:292)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: Caused by: java.lang.ExceptionInInitializerError
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at java.lang.Class.classForName(Native Method)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at java.lang.Class.forName(Class.java:237)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at java.lang.Class.forName(Class.java:183)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at it.gerdavax.android.bluetooth.LocalBluetoothDevice$BluetoothSocketImpl.init(LocalBluetoothDevice.java:534)
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: ... 4 more
09-08 09:07:29.977 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: Caused by: java.lang.UnsatisfiedLinkError: classInitNative
09-08 09:07:29.987 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at android.bluetooth.RfcommSocket.classInitNative(Native Method)
09-08 09:07:29.987 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: at android.bluetooth.RfcommSocket.<clinit>(RfcommSocket.java:152)
09-08 09:07:29.987 2170-3998/com.suke_blog.androidbluetoothtest W/System.err: ... 8 more

 

IS01のBluetooth実装を調べる

よそ様のブログを拝見していると、どうやらIS01にはライブラリが内部で使用しているRfcommSocketが実装されていないようです。しかし、どうもシンボルテーブルを見ると別なクラスとして実装されている可能性があるようです。

libandroid_runtime.soのシンボルテーブルを見てみると、たしかにBluetoothSocketなるシンボルがあります。

[SHELL]

$ objdump -T /system/lib/libandroid_runtime.so
......
0006f9fc g DF .text 00000108 _ZN7android42register_android_bluetooth_BluetoothSocketEP7_JNIEnv
......

[/SHELL]

BluetoothSocketはAndroid 2.0以降に実装されたクラスで、公式のAndroid1.6には存在しないクラスです。参考にさせて頂いたブログでOESFがリリースした"Embedded Master1"に似たような実装があるとの情報がありましたが、残念ながらOESFのリポジトリは非公開になっていました。どうも会員のみに公開されている模様。会費は個人からすると高額でなんともなりません。

仮にBluetoothの実装がどうなっているにせよ、IS01に初めから入っているアプリケーションがBluetoothを利用しているからにはどこかに実装があるはずです。IS01から以下のディレクトリをコピーして調べてみることにしました。frameworkはbaksmali/smaliやdex2jarを使ってdecompileです。

  • /system/lib
  • /system/framework

すると、frameworkにandroid.bluetoothパッケージが含まれていることが分かりました。クラス構成を眺めていると、どうやらAndroid2.xに近いものがあります。ただし、リファレンスの公式APIとはメソッドが異なります。
期待出来そうなクラスが見つかりました。

  • BluetoothSocket
  • RfcommSocket

このうち、RfcommSocketはframeworkにクラスこそあるものの肝心の処理が実装されていません。BluetoothSocketはライブラリにシンボルがあったので期待出来そうです。そういえば、IS01が発売されるより前にAndroid2.xがリリースされていたはずです。憶測ですが、いくつかの機能をAndroid2.xから持ってきたのかもしれません。

  • IS01の発売日:2010年6月30日
  • Android 2.2(Froyo)リリース日:2010年3月20日

参考までに、Android1.6と2.0のコードは以下サイトで見ることができます。

 

テストプログラムで試行錯誤する

公式リファレンス通りの実装ではないものの類推(あてずっぽうとも言う)出来そうな感じだったので、テストプログラムを作って動きを見てみることにしました。

ペアリングしたBluetooth Deviceリストの取得処理などはライブラリで動作していたので、ライブラリが使える部分はそのまま使うことにしてRFCOMM処理の部分だけを作ります。この部分はIS01独自実装で非公開なのでそのままではbuild出来ません。非公開のAPIを利用する方法として以下の手段があります。

  • Javaのreflectionを使用する
  • ビルド時にだけ参照するライブラリファイル(jar)を作る

今回はjarファイルを作って対処しました。Android SDKにあるライブラリファイル(SDK/platforms/android-22/android.jar)のandroid.bluetoothパッケージをIS01から取り出したものとそっくり入れ替えます。これでbuildは通るようになります。

入れ替えるSDKはAndroid 5.1(API-22)をベースにしました。Android Studio 2.xで古い端末向けにbuildする際にいつも使用しているためです。

これが正しいやり方なのか自信はありませんが、この内容でLinux環境に構築したRFCOMMサーバと通信することが出来ました(短時間のテストなので安定しているかは不明です)。

BluetoothSocketクラスはIS01 Frameworkのものを使用

パッケージが混じっていて読みづらくて申し訳ありませんが、backport.android.bluetoothパッケージが非公式のbackport-android-bluetoothライブラリのものです。RFCOMMを使うためにBluetoothSocketクラスのみIS01本体のライブラリを参照しています。
[JAVA]
import android.bluetooth.BluetoothSocket;

import backport.android.bluetooth.BluetoothAdapter;
import backport.android.bluetooth.BluetoothDevice;
import backport.android.bluetooth.SdpHelper;

public class MainFragment extends Fragment {
private static final String LOG_TAG = MainFragment.class.getSimpleName();
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

private Spinner mSpinnerDevice = null;
private EditText mEditTextMessage = null;
private EditText mEditTextLog = null;

private BluetoothSocket mBluetoothSocket = null;
private SdpHelper mSdp = null;
private Set<BluetoothDevice> mPairedDevices = null;

private InputStream mBluetoothInputStream = null;
private OutputStream mBluetoothOutputStream = null;

private MessageReceiveAsyncTask mReceiveTask = null;

[/JAVA]

ペアリングしたデバイス一覧の取得処理

この部分はオリジナルのbackport-android-bluetoothライブラリを使った処理です。

 

[JAVA]

//get bluetooth device list
String[] deviceList = getBluetoothDeviceList();

private String[] getBluetoothDeviceList(){
ArrayList<String> result = new ArrayList<String>();

//get bluetooth device list
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null) {
if (adapter.isEnabled()) {
mPairedDevices = adapter.getBondedDevices();
if (mPairedDevices.size() > 0) {
for (BluetoothDevice device : mPairedDevices) {
result.add(device.getAddress());
}
}
} else {
Toast.makeText(getActivity(), "Bluetooth is disabled.", Toast.LENGTH_SHORT).show();
Log.v(LOG_TAG, "Bluetooth is disabled.");
}
} else {
Toast.makeText(getActivity(), "Bluetooth is not available..", Toast.LENGTH_SHORT).show();
Log.v(LOG_TAG, "Bluetooth is not available");
}

return (String[])result.toArray(new String[0]);
}

[/JAVA]

RFCOMM接続処理

IS01でRFCOMMを使うのに試行錯誤した部分です。BluetoothSocketクラスはbackportライブラリではなく、IS01のandroid.bluetoothパッケージのものを使用します。

  1. Socket生成
    mBluetoothSocket = android.bluetooth.BluetoothSocket.createRfcommSocket(targetDevice.getAddress(), port);
  2. 接続
    mBluetoothSocket.connect();

接続時に例外が発生する場合は接続先(PC環境)でRFCOMMが立ち上がっているか、SDP DaemonにSPPが追加されているかを確認してください。Bluetoothは接続時にSDPを使ってSPPをサポートしているかの問い合わせを行いますので、SDPにSPPが無いと接続できません。

[JAVA]

 

/**
* RFCOMM connect to paired bluetooth device.
*/
private void connectDevice(String addr){
BluetoothDevice targetDevice = null;

for(BluetoothDevice device : mPairedDevices){
if(device.getAddress().equals(addr)){
targetDevice = device;
break;
}
}

if(targetDevice == null){
return;
}

if(mSdp == null){
mSdp = new SdpHelper(targetDevice, SPP_UUID);
}

int port = 0;
try{
port = mSdp.doSdp();
}catch (Exception e){
Log.e(LOG_TAG, "SDP fail", e);
}

try{
mBluetoothSocket = android.bluetooth.BluetoothSocket.createRfcommSocket(targetDevice.getAddress(), port);
if(mBluetoothSocket != null) {
mBluetoothSocket.connect();
mBluetoothInputStream = mBluetoothSocket.getInputStream();
mBluetoothOutputStream = mBluetoothSocket.getOutputStream();
}
}catch (Exception e){
Log.e(LOG_TAG, "createRfcommSocket fail", e);
}
}

[/JAVA]

 

メッセージの送受信処理

テストなので、送信処理はお手軽にUIスレッドから実行しました。受信処理はAsyncTaskを使ってバックグラウンドで処理を行います。

[JAVA]

 

/**
* RFCOMM send message
*/
private void sendMessage(String msg){
if(mWriter == null) {
mWriter = new OutputStreamWriter(mBluetoothOutputStream);
}

try{
mWriter.write(msg);
mWriter.flush();
}catch (Exception e){
Log.e(LOG_TAG, "Write stream failed.", e);
}
}
/**
* RFCOMM Receive task
*/
class MessageReceiveAsyncTask extends AsyncTask<InputStream, String, Void>{
private final String LOG_TAG = MessageReceiveAsyncTask.class.getSimpleName();
private final int DURATION = 1000;
BufferedReader mReader = null;
InputStream mInput = null;

@Override
protected Void doInBackground(InputStream... params) {
mInput = params[0];
byte[] buffer = new byte[256];
Log.v(LOG_TAG, "start doInBackground.");

while(!isCancelled()){
try {

if (mInput.available() > 0) {
int cnt = mInput.read(buffer);
String str = new String(buffer,0,cnt);

publishProgress(str);
} else {
Thread.sleep(1000);
}
}catch (Exception e){
Log.e(LOG_TAG, "loop failed", e);
}
}

Log.v(LOG_TAG, "finish doInBackground.");

return null;
}

@Override
protected void onProgressUpdate(String... values) {
super.onProgressUpdate(values);

appendLogText(values[0]);
}

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

try {
mReader.close();
}catch (Exception e){
Log.e(LOG_TAG, "onCancelled fail", e);
}
}
}
}

private void appendLogText(String str){
mEditTextLog.append(str);
}
[/JAVA]

 

 

backport-android-bluetoothライブラリをIS01でも動作するよう改造する

ひとまずIS01でもRFCOMMが使用可能だと確認出来ました。ただ、この処理だと(恐らくは)IS01しか動きません。ただでさえAndroid1.6というガラクタ(失礼)をターゲットとしたアプリケーションを作っているのに、マニアックな一機種だけしか動かないのでは自分以外誰も使わない可能性が大です。

そこで、backport-android-bluetoothライブラリに少し手を加えてIS01とほかの機種の処理を分けるようにしてみました(LazyLoadパターンというらしい)。IS01と他機種間の差異をライブラリ内で吸収しているので少しは汎用的に使えるアプリケーションが作れる、ハズです。IS01以外のAndroid1.6端末を持っていないので未確認ですが。。。

変更した箇所は以下の通りです。

  • BluetoothSocketクラスをabstractに修正
  • BluetoothScoketクラスを継承したBluetoothSocketBaseクラスを追加(元々の処理)
  • BluetoothScoketクラスを継承したBluetoothSocketIS01クラスを追加(IS01の処理)
  • 上記に伴いSdpHelperクラスをinner -> publicに修正
  • 上記変更に伴いちょこっと修正

 

BluetoothSocket.java
[JAVA]
package backport.android.bluetooth;

import android.bluetooth.RfcommSocket;
import android.os.Build;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public abstract class BluetoothSocket implements Closeable {

static final int DEFAULT_CHANNEL = 30;
static final int EBADFD = 77;
static final int EADDRINUSE = 98;

private static final String MODEL_IS01 = "IS01";

final RfcommSocket mRfcommSocket = null;

protected BluetoothSocket(){

}

public static BluetoothSocket getInstance(BluetoothDevice device, UUID uuid){
if(Build.MODEL.equals(MODEL_IS01)){
return new BluetoothSocketIS01(device, uuid);
}else{
return new BluetoothSocketBase(device, uuid);
}
}

public abstract void close() throws IOException;

public abstract void connect() throws IOException;

public abstract InputStream getInputStream() throws IOException;

public abstract OutputStream getOutputStream() throws IOException;

public abstract BluetoothDevice getRemoteDevice();

abstract int bindListen();

abstract BluetoothSocket accept(int timeout) throws IOException;

}

[/JAVA]

BluetoothSocketIS01.java
[JAVA]
package backport.android.bluetooth;

import android.bluetooth.*;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class BluetoothSocketIS01 extends BluetoothSocket {
private static final String TAG = BluetoothSocketIS01.class.getSimpleName();
private/* final */BluetoothDevice mRemoteDevice;

android.bluetooth.BluetoothSocket mRfcommSocket = null;

private InputStream mInStream;

private OutputStream mOutStream;

private final SdpHelper mSdp;

private int mPort = -1;

/** prevents all native calls after destroyNative() */
private boolean mClosed;

/** protects mClosed */
private final ReentrantReadWriteLock mLock;

public BluetoothSocketIS01(BluetoothDevice remDev, UUID uuid) {
mRemoteDevice = remDev;

if (uuid == null) {

mSdp = null;
} else {

if (remDev != null) {

mSdp = new SdpHelper(remDev, uuid);
} else {

mSdp = null;
}
}

mClosed = false;
mLock = new ReentrantReadWriteLock();
}

public void close() throws IOException {

// abort blocking operations on the socket
mLock.readLock().lock();
try {
if (mClosed)
return;
if (mSdp != null) {
mSdp.cancel();
}

try{
mInStream.close();
}catch (IOException e){
}

try{
mOutStream.close();
}catch (IOException e){
}

} finally {
mLock.readLock().unlock();
}

// all native calls are guaranteed to immediately return after
// abortNative(), so this lock should immediatley acquire
mLock.writeLock().lock();
try {
mClosed = true;
mRfcommSocket.close();
} finally {
mLock.writeLock().unlock();
}
}

public void connect() throws IOException {

mLock.readLock().lock();
try {
if (mClosed)
throw new IOException("socket closed");

if (mSdp != null) {
mPort = mSdp.doSdp(); // blocks
}

mRfcommSocket = android.bluetooth.BluetoothSocket.createRfcommSocket(mRemoteDevice.getAddress(), mPort);
mRfcommSocket.connect();
}catch (Exception e){
Log.e(TAG, "createRfcommSocket failed", e);
} finally {
mLock.readLock().unlock();
}
}

public InputStream getInputStream() throws IOException {

mInStream = mRfcommSocket.getInputStream();

return mInStream;
}

public OutputStream getOutputStream() throws IOException {

mOutStream = mRfcommSocket.getOutputStream();

return mOutStream;
}

public BluetoothDevice getRemoteDevice() {

return mRemoteDevice;
}

int bindListen() {
return -1;
}

BluetoothSocketIS01 accept(int timeout) throws IOException {
mLock.readLock().lock();
try {
if (mClosed) {
throw new IOException("socket closed");
}
} finally {
mLock.readLock().unlock();
}
//listenUsingRfcommOn(int channel)
android.bluetooth.BluetoothServerSocket socket =
android.bluetooth.BluetoothServerSocket.listenUsingRfcommOn(1);

// なぜか-1(infinity)を指定すると、closeできないのでループでinfinityな感じにする.
int tmpTimeout = (timeout > -1) ? timeout : 500;

android.bluetooth.BluetoothSocket connectedSocket = null;
for (;;) {
connectedSocket = socket.accept(tmpTimeout);

if (timeout > -1) {
break;
}

mLock.readLock().lock();
try {
if (mClosed) {
return null;
}

} finally {
mLock.readLock().unlock();
}

if(connectedSocket != null){
break;
}
}

BluetoothSocketIS01 result = new BluetoothSocketIS01(null, null);
result.mRfcommSocket = connectedSocket;

return result;
}
private String obtainAddress(RfcommSocket rfcommSocket) {
String addr = null;

return addr;
}

}
[/JAVA]

この修正したライブラリを使うことで、IS01でも他機種と同様に接続が出来るようになります。

[JAVA]

// for modified backport bluetooth library
private void connectDevice(String addr){
    BluetoothDevice targetDevice = null;

    for(BluetoothDevice device : mPairedDevices){
        if(device.getAddress().equals(addr)){
            targetDevice = device;
            break;
        }
    }

    if(targetDevice == null){
        return;
    }

    try{
        mBluetoothSocket = targetDevice.createRfcommSocketToServiceRecord(SPP_UUID);
        if(mBluetoothSocket != null){
            mBluetoothSocket.connect();
            mBluetoothInputStream = mBluetoothSocket.getInputStream();
            mBluetoothOutputStream = mBluetoothSocket.getOutputStream();

            mReceiveTask = new MessageReceiveAsyncTask();
            mReceiveTask.execute(mBluetoothInputStream);
        }
    }catch (Exception e){
        Log.e(LOG_TAG, "createRfcommSocketToServiceRecord failed", e);
    }
}

[/JAVA]

 

backport.bluetooth.androidライブラリを使ってテストアプリケーションを作る

ようやくここまで来ました(IS01ユーザ以外はそうでもない?)。早速、簡単なテストプログラムを作成してPC環境と通信してみました。

プロジェクト一式は以下にアップロードしています。

プロジェクトはIS01向けに修正したライブラリを含んでいますが、こちらは記事に書いたようにSDKに手を加えないとBuild出来ません。ライブラリ処理を修正するのでなければ、jarファイルをダウンロードしてプロジェクトに追加すればテストアプリケーションをBuild出来るかと思います。

修正版backport.bluetooth.andoroidライブラリのjarファイルは以下にあります。IS01向けアプリケーションでRFCOMMを使われる方は使ってみてください。

以下がテストプログラムの画面です。
ペアリング済みのデバイス一覧から接続先を選択して"connect"ボタンで接続されます。メッセージを送信するには画面下のテキストボックスに入力して"send"ボタンです。受信メッセージは中央に表示されます。

is01_bluetooth_spp_sample_screenshot_2.png

以下はテストで使用したLinux環境のスクリーンショットです。

 VirtualBox_Ubuntu-16.04.1_19_09_2016_18_07_59.png

 

参考

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください