-
Notifications
You must be signed in to change notification settings - Fork 23
DevicePlugin Manual for Android Studio 200
本ドキュメントでは、DeviceConnect Codegenを使用したDeviceConnectプラグインの開発方法について説明します。
DeviceConnect Codegenは、Swaggerの定義ファイルを元にプラグインのスケルトンコードなどを出力するツールになります。
詳しくは、こちらをご参照ください。
なお、下記に関する知識のある読者を対象とし、これらの解説は省略します。
- Java
- Android フレームワーク
- Android Studio
- Swagger
DeviceConnect Codegenによって生成されるプラグイン(のスケルトンコード)は、DeviceConnectプラグインSDKに依存します。DeviceConnectプラグインSDKのインポート設定は、下記のように build.gradle ファイル上に自動生成されます。
repositories {
maven { url 'https://raw.githubusercontent.com/DeviceConnect/DeviceConnect-Android/main/dConnectSDK/dConnectSDKForAndroid/repository/' }
maven { url 'https://raw.githubusercontent.com/DeviceConnect/DeviceConnect-Android/main/dConnectDevicePlugin/dConnectDevicePluginSDK/repository/' }
}
dependencies {
compile 'org.deviceconnect:dconnect-device-plugin-sdk:2.x.x
}
DeviceConnectプラグインSDKのバージョンをアップデートしたい場合は、上記の 2.x.x の部分を手動で変更し、Gradle Syncを実行してください。
ここでは、プラグインを開発するために必要な準備を説明します。
- Android Studio 2.2.1以降
- DeviceConnect Codegen
Android Studioは、ここからインストールをしてください。
DeviceConnect Codegenは、ここからインストールをしてください。
DeviceConnect Codegenをインストールする以下のようディレクトリが生成されます。
DeviceConnect Codegenを使用して、プラグインのスケルトンコードを出力します。
ここでは、DeviceConnect Codegenの簡単な使い方を説明します。
sample-profile-specsフォルダに、プラグインで使用したいAPIを定義したJSONファイルを配置します。配置するJSONファイルは、Swagger2.0でAPIを定義したファイルになります。
標準で仕様を策定しているJSONファイルは、standard-profile-specsフォルダに格納してありますので、こちらからコピーして使用してください。
JSONファイルを配置するディレクトリはsample-profile-specsフォルダから変更することもできます。
この後にDeviceConnect Codegenで詳しく説明します。
android-plugin.sh に DeviceConnect Codegen の設定を行います。
# スケルトンコード種別: Androidプラグイン
LANG="deviceConnectAndroidPlugin"
出力するスケルトンコードの種類を設定します。
ここでは、Androidプラグインを出力しますので、deviceConnectAndroidPluginを指定します。
# プロファイル定義ファイルのディレクトリ
SPEC="./sample-profile-specs/swagger.json"
プラグインがサポートするAPIの定義ファイルを指定します。
JSON形式もしくはYAML形式のファイルを指定することができます。
# スケルトンコード出力先
OUTPUT_DIR="./output/Android/MyPlugin"
スケルトンコードを出力する先のディレクトリを指定します。
指定されたディレクトリが存在しない場合には、作成します。
指定されたディレクトリが存在する場合には、上書きします。
# Androidプラグインのパッケージ名
PACKAGE_NAME="com.mydomain.myplugin"
Androidプラグインのパッケージ名を指定します。
Androidアプリケーションのパッケージ名になりますので、特殊文字などのパッケージ名に使用できない文字は指定できません。
# Androidプラグインの表示名
DISPLAY_NAME="MyPlugin"
Androidプラグインのアプリケーション名を指定します。
$ cd samples
$ ./android-plugin.sh
シェルを実行することで、OUTPUT_DIR で指定したディレクトリにスケルトンコードを出力します。
出力されたスケルトンコードは以下のようになります。
出力されたスケルトンコードを Android Studio にインポートします。
Android Studioを起動し、Open an existing Android Studio projectを選択します。
Open File or Projectのダイアログ上で、DeviceConnect Codegenから出力したディレクトリを指定します。
プラグインのプロジェクトをインポートするとプロジェクトにエラーが発生して、実行することができません。
Android Studioのメニューからpluginを選択してEdit Configurationを選択して、プロジェクトの設定ダイアログを表示します。
Launch Optionsの設定をDefault ActivityからNothingに変更して、OKを押下します。
pluginからバツのマークが消えますので、Android Studioから実行ボタンを押下します。
これで、Android端末にプラグインがインストールされます。
Device Connect Managerを起動し、サービスの一覧にMyPlugin Serviceが表示されていれば、インストールに成功しています。
MyPlugin Servcieを押下して、各サービスの確認画面へ遷移します。
batteryを押下して、各プロファイルの確認画面へ遷移します。
GET /gotapi/batteryを押下して、APIの実行メニューを開きます。
Send Requestボタンを押下して、APIを実行します。
レスポンスに値が返ってくることが確認できます。
ここでは、プラグインが管理するサービスの実装方法を説明します。
まず、プラグインが管理するサービスは、DConnectServiceを作成します。
次に、実装したDConnectServiceをDConnectMessageService#getServiceProvider()で取得できるDConnectServiceProviderに追加することで管理します。
Service Discoveryでサービスが検索された時に、Device Connect Plugin SDKはDConnectServiceProviderに登録されているDConnectServiceの一覧を返却します。
MyMessageService.javaで以下のようにしてサービスを追加しています。
DConnectService service = new DConnectService("my_service_id");
// TODO サービス名の設定
service.setName("MyPlugin Service");
// TODO サービスの使用可能フラグのハンドリング
service.setOnline(true);
// TODO ネットワークタイプの指定 (例: BLE, Wi-Fi)
service.setNetworkType(NetworkType.UNKNOWN);
service.addProfile(new MyBatteryProfile());
service.addProfile(new MyCanvasProfile());
service.addProfile(new MyDeviceOrientationProfile());
getServiceProvider().addService(service);複数のサービスを管理する場合には、必要なサービス分だけ追加します。
DConnectServiceは、コンストラクタに渡すサービスIDで管理していますので、ユニークな値を指定します。
また、サービスIDが重複した場合には上書きします。
DConnectService#addProfile()で追加したプロファイルが、そのサービスで使用できるプロファイルになります。
サービスで必要なプロファイルを実装して、追加します。
周辺機器は常に接続されている訳ではなく、接続が切れている場合があります。
それらを識別するためにDConnectService#setOnline()で接続状態を管理します。
trueの場合には接続状態、falseの場合には切断状態を表します。
サービスの接続状態に合わせて、DConnectService#setOnline()の値を設定します。
ここでは、DeviceConnect Codegenで出力したプロファイルの実装方法を説明します。
標準化を行なっている各プロファイルの定義は、こちらで行なっています。
また、各プロファイルの定義は、HTTPリクエストの形式で行われています。
プラグインにリクエストが配送されてくる時には、Intentになっていますので、その変換方法については、こちらのManagerとプラグインの通信についてを参照してください。
確認したバッテリーの情報を取得するAPIは、MyBatteryProfile.javaで登録されている以下の関数で実装しています。
// GET /gotapi/battery/
addApi(new GetApi() {
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
// TODO ここでAPIを実装してください. 以下はサンプルのレスポンス作成処理です.
setResult(response, DConnectMessage.RESULT_OK);
Bundle root = response.getExtras();
root.putBoolean("charging", false);
root.putLong("dischargingTime", 0L);
root.putFloat("level", 0.0f);
root.putLong("chargingTime", 0L);
response.putExtras(root);
return true;
}
});responseに設定している値を変更することで、アプリケーションに返されるレスポンスの値が変更されます。
実際にlevelの値を以下のように変更してみて確認してください。
root.putFloat("level", 0.5f);
3.4. スケルトーンコードの実行で、行った手順を再度を行いバッテリーの値が変更されていれば、修正成功です。
レスポンスは、非同期に返却することもできます。
関数の返り値をfalseにして、非同期に DConnectMessageService#sendResponse(Intent) もしくはDConnectProfile#sendResponse(Intent)を使用してレスポンスを返却します。
以下のサンプルでは、1秒後にレスポンスを返却します。
// GET /gotapi/battery/
addApi(new GetApi() {
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
Executors.newSingleThreadExecutor().submit(new Runnable() {
@Override
public void run() {
// 1秒待つ
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setResult(response, DConnectMessage.RESULT_OK);
Bundle root = response.getExtras();
root.putBoolean("charging", false);
root.putLong("dischargingTime", 0L);
root.putFloat("level", 0.0f);
root.putLong("chargingTime", 0L);
response.putExtras(root);
// レスポンスを返却
sendResponse(response);
}
});
// 非同期でレスポンスを返却する場合はfalseにする
return false;
}
});処理に時間がかかり即座にレスポンスを返却できない場合には、上記のように非同期で処理を行い、最後にレスポンスを返却することができます。
ただし、Device Connect Managerは、プラグインから30秒以内返答がない場合に、アプリケーションにタイムアウトエラーを返却します。
プラグインは、出来るだけ速くレスポンスを返却するようにしてください。
エラーレスポンスを返却したい場合には、MessageUtilsを使用します。
MessageUtilsには、Device Connect で定義されているエラーを簡単に設定できるメソッドが用意されています。
以下のサンプルではパラメータ異常の場合のエラーを設定します。
// GET /gotapi/battery/
addApi(new GetApi() {
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
// パラメータ異常エラーを設定
MessageUtils.setInvalidRequestParameterError(response, "Invalid a parameter.");
return true;
}
});プラグインは、エラーに合わせてエラーコードとエラーメッセージを指定して返却するようにしてください。
ここでは、送信したメッセージを返却するだけの簡単なプロファイルを作成します。
以下のsample-profile-specs/swagger.jsonに以下の定義を追加して、DeviceConnect Codegenを実行します。
{
"swagger": "2.0",
"info": {
"title": "Device Connect API",
"version": "2.0.0",
"description": ""
},
"consumes": [],
"basePath": "/gotapi",
"paths": {
"/echo": {
"get": {
"x-type": "one-shot",
"summary": "Echo",
"description": "送られてきたメッセージを、そのまま返却するAPI",
"parameters": [
{
"name": "serviceId",
"in": "query",
"required": true,
"type": "string"
},
{
"name": "message",
"in": "query",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "Echo",
"schema": {
"$ref": "#/definitions/EchoResponse"
}
}
}
}
},
// ...省略 ...
},
"definitions": {
"EchoResponse": {
"type": "object",
"allOf": [
{
"$ref": "#/definitions/CommonResponse"
},
{
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string",
"description": "メッセージ"
}
}
}
]
},
// ...省略 ...
}
}DeviceConnect Codegenで生成されたプロジェクトに新たにMyEchoProfile.javaが追加されています。
echo.jsonは、Swagger2.0の仕様に沿って作成しています。
Swagger2.0の仕様を確認したい場合は、こちらをご参照してください。
package com.mydomain.myplugin.profiles;
import android.content.Intent;
import android.os.Bundle;
import org.deviceconnect.android.profile.DConnectProfile;
import org.deviceconnect.android.profile.api.GetApi;
import org.deviceconnect.message.DConnectMessage;
public class MyEchoProfile extends DConnectProfile {
public MyEchoProfile() {
// GET /gotapi/echo/
addApi(new GetApi() {
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
String message = (String) request.getExtras().get("message");
// TODO ここでAPIを実装してください. 以下はサンプルのレスポンス作成処理です.
setResult(response, DConnectMessage.RESULT_OK);
Bundle root = response.getExtras();
root.putString("message", "test");
response.putExtras(root);
return true;
}
});
}
@Override
public String getProfileName() {
return "echo";
}
}出力された MyEchoProfile.java を開き、以下の箇所を修正します。
root.putString("message", "test");を以下のように修正します。
root.putString("message", message);上記の修正を行ったらプラグインをビルドし、インストールを行います。
Device Connect Managerを起動して、MyPlugin Serviceを開きます。
echoプロファイルが追加されていますので、echoのアイコンを押下して、プロファイル確認画面を開きます。
messageに入力した文字列がレスポンスのJSONに返却されていることが確認できます。
プラグインの設定画面は、MySystemProfile.javaで実装しているgetSettingPageActivity()で返却しているActivityを表示します。
スケルトンコードでは、MySettingActivity.javaを設定画面として表示します。
このActivity上で、周辺機器との接続や切断の管理を実装します。
設定画面が不要な場合には、getSettingPageActivity()でnullを返却します。
package com.mydomain.myplugin.profiles;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import com.mydomain.myplugin.MySettingActivity;
import org.deviceconnect.android.profile.SystemProfile;
public class MySystemProfile extends SystemProfile {
@Override
protected Class<? extends Activity> getSettingPageActivity(final Intent request, final Bundle param) {
// TODO 設定画面が不要な場合、null を返却してください.
return MySettingActivity.class;
}
}設定画面からMyMessageServiceを呼び出して、情報を共有する場合には、以下のようにしてMyMessageServiceをバインドします。
package com.mydomain.myplugin;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import org.deviceconnect.android.message.DConnectMessageService;
public class MySettingActivity extends Activity {
private MyMessageService mMyMessageService;
private boolean mBound;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);
// TODO デバイスとの接続等で手動操作が必要な場合は、設定画面を実装してください.
// TODO 不要な場合は削除してください.
}
@Override
protected void onResume() {
super.onResume();
bindMessageService();
}
@Override
protected void onPause() {
unbindMessageService();
super.onPause();
}
private void bindMessageService() {
if (!mBound) {
Intent intent = new Intent(getApplicationContext(), MyMessageService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
}
private void unbindMessageService() {
if (mBound) {
unbindService(mConnection);
mBound = false;
}
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName name, final IBinder service) {
mMyMessageService = (MyMessageService) ((DConnectMessageService.LocalBinder) service).getMessageService();
mBound = true;
}
@Override
public void onServiceDisconnected(final ComponentName name) {
mBound = false;
}
};
}設定画面において、接続しているサービスの管理を行う場合には、MyMessageServiceにバインドします。
あとは、管理に必要なメソッドをMyMessageServiceに実装してMySettingActivityから呼び出して使用します。
イベントの管理を行う上でEventManagerというユーティリティクラスを用意していますので、ここでは、EventManagerの使用方法を説明します。
イベントを発生させるためには、以下のようなシーケスになるように実装する必要があります。
イベントを登録する場合には、以下の関数を使用します。
EventManager#addEvent(request)
- 'request' : PutApi#onRequest()の引数に渡されてくるリクエストを渡します。
// PUT /gotapi/battery/onChargingChange
addApi(new PutApi() {
@Override
public String getAttribute() {
return "onChargingChange";
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
Long interval = parseLong(request, "interval");
if (interval == null) {
interval = 1000L;
}
EventError error = EventManager.INSTANCE.addEvent(request);
switch (error) {
case NONE:
setResult(response, DConnectMessage.RESULT_OK);
// 以下、サンプルのイベントの定期的送信を開始.
String taskId = serviceId;
TimerTask task = new TimerTask() {
@Override
public void run() {
Event event = EventManager.INSTANCE.getEvent(request);
Intent message = EventManager.createEventMessage(event);
Bundle root = message.getExtras();
Bundle battery = new Bundle();
battery.putBoolean("charging", false);
root.putBundle("battery", battery);
message.putExtras(root);
sendEvent(message, event.getAccessToken());
}
};
startTimer(taskId, task, interval);
break;
case INVALID_PARAMETER:
MessageUtils.setInvalidRequestParameterError(response);
break;
default:
MessageUtils.setUnknownError(response);
break;
}
return true;
}
});このサンプルでは、1秒ごとにバッテリーのchargingイベントを送信します。
イベントを解除する場合には、以下の関数を使用します。
EventManager#removeEvent(request)
- 'request' : PutApi#onRequest()の引数に渡されてくるリクエストを渡します。
// DELETE /gotapi/battery/onChargingChange
addApi(new DeleteApi() {
@Override
public String getAttribute() {
return "onChargingChange";
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
EventError error = EventManager.INSTANCE.removeEvent(request);
switch (error) {
case NONE:
setResult(response, DConnectMessage.RESULT_OK);
// 以下、サンプルのイベントの定期的送信を停止.
String taskId = serviceId;
stopTimer(taskId);
break;
case INVALID_PARAMETER:
MessageUtils.setInvalidRequestParameterError(response);
break;
case NOT_FOUND:
MessageUtils.setUnknownError(response, "Event is not registered.");
break;
default:
MessageUtils.setUnknownError(response);
break;
}
return true;
}
});イベントの解除については、アプリケーション開発者が停止要求を行うのを忘れたり、アプリケーションが強制終了するなどされて停止要求が来ない可能性があります。
その為に、アプリケーションからの停止要求以外にもイベントを解除するタイミングを用意しています。
-
MyMessageService#onManagerTerminated: Device Connect Managerがアンインストールされたことを通知します。 -
MyMessageService#onManagerEventTransmitDisconnected: WebSocketの接続が切断されたことを通知します。 -
MyMessageService#onDevicePluginReset: プラグインのリセット要求を通知します。
これらのタイミングにおいて、イベントの解除を行うことで不要になったセンサーを停止します。
登録されているイベントは、以下の関数で取得することができます。
EventManager#getEventList(serviceId, profile, inter, attribute)
- 'serviceId' : イベントを発生させるサービスのID。
- 'profile' : 発生させるイベントのプロファイル名。
- 'inter' : 発生させるイベントのインターフェース。(省略する場合にはnull)
- 'attribute' : 発生させるイベントのあトリビュート。(省略する場合にはnull)
送信するイベントを取得し、EventManager#createEventMessage(Event)でEventからIntentを作成します。
作成したイベント用Intentは、DConnectProfile#sendEvent(Intent, String)を使用して、送信します。
List<Event> events = EventManager.INSTANCE.getEvent(serviceId, "battery", null, "onChargingChange");
for (Event event : events) {
Intent message = EventManager.createEventMessage(event);
Bundle root = message.getExtras();
Bundle battery = new Bundle();
battery.putBoolean("charging", false);
root.putBundle("battery", battery);
message.putExtras(root);
sendEvent(message, event.getAccessToken());
}ここでは、リソースをプラグインからアプリケーションに提供する方法を説明します。
リソースをアプリケーションに返却する為に、MediaStreamRecordingを実装します。
standard-profile-specs/deviceOrientation.jsonをsample-profile-specsにコピーして、DeviceConnect Codegenでプロジェクトを出力します。
出力されたプロジェクトのplugin/src/main/res/drawable-xxhdpi/ic_launcher.pngをplugin/src/main/assetsにコピーします。
この画像をアプリケーションに渡すサンプルを作成します。
ContentProviderを利用して、リソースをプラグインからアプリケーションに返却する方法を説明します。
ContentProviderを利用するにあたり、Device Connect SDKでは、FileManagerというユーティリティクラスを用意しています。
この以下のサンプルでは、このFileManagerを使用してアプリケーションにリソースを渡します。
ContentProviderを利用しますので、以下のようにAndroidManifext.xmlにproviderタグを追記します。
android:authoritiesは、ユニークになるようにパッケージ名などを利用します。
また、ファイル保存先のフォルダ指定を行うfilelocation.xmlをmeta-dataタグで指定します。
<!-- ContentProvider設定 -->
<provider
android:name="org.deviceconnect.android.provider.FileProvider"
android:authorities="org.deviceconnect.android.deviceplugin.myservice.provider"
android:exported="true" >
<meta-data
android:name="filelocation"
android:resource="@xml/filelocation" />
</provider>保存先のフォルダ設定するファイルは、res/xml/filelocation.xmlに以下の設定を保存します。
<?xml version="1.0" encoding="UTF-8"?>
<file-locations xmlns:android="http://schemas.android.com/apk/res/android" >
<internal-location path="myservice" />
</file-locations>internal-locationタグでは、アプリケーションフォルダ内にmyserviceというフォルダを作成して、ファイルを保存します。
SDカードなどの外部フォルダに保存したい場合には、external-locationを指定します。
SDカードに保存する場合には、パーミッションが追加で必要になります。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
ただし、SDカードにデータを保存する場合には、他のアプリからも参照されますので注意が必要になります。
MyMediaStreamRecordingProfileに以下のように実装を追加します。
ここでは、わかりやすく写真撮影APIのみを実装し、他のAPIは削除します。
package com.mydomain.myplugin.profiles;
import android.content.Intent;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.DConnectProfile;
import org.deviceconnect.android.profile.api.PostApi;
import org.deviceconnect.android.provider.FileManager;
import org.deviceconnect.message.DConnectMessage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyMediaStreamRecordingProfile extends DConnectProfile {
private FileManager mFileMgr;
private String mUri;
public MyMediaStreamRecordingProfile() {
// POST /gotapi/mediaStreamRecording/takePhoto
addApi(new PostApi() {
@Override
public String getAttribute() {
return "takePhoto";
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
String target = (String) request.getExtras().get("target");
if (mFileMgr == null) {
mFileMgr = new FileManager(getContext());
mFileMgr.saveFile("icon.png", getResourceData(), new FileManager.SaveFileCallback() {
@Override
public void onSuccess(@NonNull String s) {
mUri = s;
setResult(response, DConnectMessage.RESULT_OK);
Bundle root = response.getExtras();
root.putString("uri", mUri);
root.putString("path", "test");
response.putExtras(root);
sendResponse(response);
}
@Override
public void onFail(@NonNull Throwable throwable) {
MessageUtils.setUnknownError(response, "Cannot save a file.");
sendResponse(response);
}
});
// 非同期でレスポンスを返却するのでfalse
return false;
} else {
setResult(response, DConnectMessage.RESULT_OK);
Bundle root = response.getExtras();
root.putString("uri", mUri);
root.putString("path", "test");
response.putExtras(root);
return true;
}
}
});
}
@Override
public String getProfileName() {
return "mediaStreamRecording";
}
public byte[] getResourceData() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
AssetManager mgr = getContext().getAssets();
InputStream in = mgr.open("ic_launcher.png");
int len;
byte[] buf= new byte[4096];
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
return out.toByteArray();
}
}FileManager#saveFile()が非同期になっていますが、これは、Android OS 6.0以降では、SDカードへのファイルアクセスにユーザ許可が必要になった為です。
MyServiceからMediaStreamRecordingを選択し、POST /gotapi/mediaStreamRecording/takePhoto を実行します。
レスポンスに、URIが返ってきますので、クリックすることで、保存した画像を表示することができます。
Webサーバを利用して、リソースをプラグインからアプリケーションに返却する方法を説明します。
レスポンスにURIを返却して、アプリから直接プラグインが立ち上げたWebサーバにアクセスして、自由なデータのやり取りを行います。
プラグインでWebサーバを立ち上げる為には、AndroidManifest.xmlに以下のパーミッションを追加します。
このパーミッションがないとHTTP通信が行えません。
<uses-permission android:name="android.permission.INTERNET"/>package com.mydomain.myplugin;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executors;
public class MyServer {
private static final String CRLF = "\r\n";
private ServerSocket mServerSocket;
private boolean mStopFlag;
private byte[] mImageData;
public void setImageData(byte[] imageData) {
mImageData = imageData;
}
public synchronized void start() throws IOException {
if (mServerSocket != null) {
throw new IllegalStateException("MyServer is already running.");
}
mServerSocket = new ServerSocket(9000);
mStopFlag = false;
Executors.newSingleThreadExecutor().submit(new Runnable() {
@Override
public void run() {
try {
startServerSocket();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
public synchronized void stop() throws IOException {
if (mServerSocket == null) {
throw new IllegalStateException("MyServer is not running.");
}
mStopFlag = true;
mServerSocket.close();
mServerSocket = null;
}
public synchronized boolean isRunning() {
return !mStopFlag && mServerSocket != null;
}
private void startServerSocket() throws IOException {
while (!mStopFlag) {
startSocket(mServerSocket.accept());
}
}
private void startSocket(final Socket socket) {
Executors.newSingleThreadExecutor().submit(new Runnable() {
@Override
public void run() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
String line = in.readLine();
while (line != null && !line.isEmpty()) {
line = in.readLine();
}
OutputStream out = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
bw.write("HTTP/1.1 200 OK" + CRLF);
bw.write("Server: MyServer" + CRLF);
bw.write("Connection: close" + CRLF);
bw.write("Content-Type: image/png" + CRLF);
bw.write(CRLF);
bw.flush();
out.write(mImageData);
out.flush();
} catch (IOException e) {
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}上記のようにWebサーバを用意します。
このWebサーバは、assetsの中にある画像データを返却するだけの簡単な作りになっています。
package com.mydomain.myplugin.profiles;
import android.content.Intent;
import android.content.res.AssetManager;
import android.os.Bundle;
import com.mydomain.myplugin.MyServer;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.DConnectProfile;
import org.deviceconnect.android.profile.api.PostApi;
import org.deviceconnect.message.DConnectMessage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyMediaStreamRecordingProfile extends DConnectProfile {
private MyServer mMyServer;
public MyMediaStreamRecordingProfile() {
// POST /gotapi/mediaStreamRecording/takePhoto
addApi(new PostApi() {
@Override
public String getAttribute() {
return "takePhoto";
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
String target = (String) request.getExtras().get("target");
if (mMyServer != null && mMyServer.isRunning()) {
try {
mMyServer.stop();
} catch (IOException e) {
e.printStackTrace();
}
}
mMyServer = new MyServer();
mMyServer.setImageData(getResourceData());
try {
mMyServer.start();
setResult(response, DConnectMessage.RESULT_OK);
Bundle root = response.getExtras();
root.putString("uri", "http://localhost:9000/");
root.putString("path", "test");
response.putExtras(root);
} catch (IOException e) {
MessageUtils.setIllegalDeviceStateError(response, "Cannot start a server.");
}
return true;
}
});
}
@Override
public String getProfileName() {
return "mediaStreamRecording";
}
public byte[] getResourceData() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
AssetManager mgr = getContext().getAssets();
InputStream in = mgr.open("ic_launcher.png");
int len;
byte[] buf= new byte[4096];
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
return out.toByteArray();
}
}MyMediaStreamRecordingProfileの写真撮影APIにおいて、Webサーバを起動して、そのURIを返却するようにします。
サンプルなので、MyServerの停止は、行っていません。
一度のアクセスが完了した時点で終了にするなどの処理が必要になります。
また、Webサーバへのアクセスに制限をかけていませんので、指定されたURIにアクセスされた場合にデータを取得されてしまいます。
他のアプリからアクセスされたくない場合には、URLパスにハッシュ値などを含めて類推できないようにする必要があります。
ContentProviderを利用する場合と同じ操作を行うことで、リソースを取得することが確認できます。
プラグインがアプリケーションからリソースを取得する場合に、いくつかの手段があります。
アプリケーションからマルチパートでデータが送信されてきた場合には、Device Connect Managerがファイルに保存して、プラグインにURIを送信します。
プラグインはContentProvider経由でリソースを取得します。
ただし、Device Connect Managerは、送られてきたリソースをいつまでも保存してはいません。
アクセスが完了してから一定時間経過して時点でリソースを削除します。
後でリソースを使用する場合には、一度プラグイン内にリソースを保存するなどして対応する必要があります。
アプリケーションからURIが送信されたきた場合には、プラグインはWebサーバ経由でリソースを取得します。
ここでは、Canvasプロファイルを実装しリソースの取得方法について説明します。
- Canvasプロファイルは、ContentProvider経由もしくはWebサーバ経由で送られてくるリソースへのURIを取得します。
- 取得したURIを引数にして、MyCanvasActivityを起動します。
- MyCanvasActivityは、URIからリソースを取得して、画面に表示します。
リソースには、ContentProvider経由、Webサーバ経由のどちらで送られてくるかは、アプリケーション次第なので、ここでは両方に対応します。
Webサーバ経由の場合には、通信が発生しますので、パーミッションにandroid.permission.INTERNETを追加します。
また、取得したリソースを画面に表示するためのMyCanvasActivityも追加します。
<uses-permission android:name="android.permission.INTERNET"/>
...省略...
<!-- Canvas画面 -->
<activity android:name=".MyCanvasActivity"/>package com.mydomain.myplugin.profiles;
import android.content.Intent;
import com.mydomain.myplugin.MyCanvasActivity;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.DConnectProfile;
import org.deviceconnect.android.profile.api.DeleteApi;
import org.deviceconnect.android.profile.api.PostApi;
import org.deviceconnect.message.DConnectMessage;
public class MyCanvasProfile extends DConnectProfile {
public MyCanvasProfile() {
// POST /gotapi/canvas/drawImage
addApi(new PostApi() {
@Override
public String getAttribute() {
return "drawImage";
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
String mimeType = (String) request.getExtras().get("mimeType");
String uri = (String) request.getExtras().get("uri");
Object data = request.getExtras().get("data");
Integer x = parseInteger(request, "x");
Integer y = parseInteger(request, "y");
String mode = (String) request.getExtras().get("mode");
if (data == null && uri == null) {
MessageUtils.setInvalidRequestParameterError(response, "data or uri is not set.");
return true;
}
// 画像のデータをActivityに送信
Intent intent = new Intent();
intent.setClass(getContext(), MyCanvasActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (uri != null) {
intent.putExtra("uri", uri);
}
if (data != null) {
intent.putExtra("data", data);
}
getContext().startActivity(intent);
// TODO ここでAPIを実装してください. 以下はサンプルのレスポンス作成処理です.
setResult(response, DConnectMessage.RESULT_OK);
return true;
}
});
// DELETE /gotapi/canvas/drawImage
addApi(new DeleteApi() {
@Override
public String getAttribute() {
return "drawImage";
}
@Override
public boolean onRequest(final Intent request, final Intent response) {
String serviceId = (String) request.getExtras().get("serviceId");
// TODO ここでAPIを実装してください. 以下はサンプルのレスポンス作成処理です.
setResult(response, DConnectMessage.RESULT_OK);
return true;
}
});
}
@Override
public String getProfileName() {
return "canvas";
}
}MyCanvasProfileを実装します。
送られてきたURIなどの情報をIntentに格納し、MyCanvasActivityを起動します。
package com.mydomain.myplugin;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ImageView;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class MyCanvasActivity extends Activity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_canvas);
setImage(getIntent());
}
@Override
protected void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
setImage(intent);
}
private void setImage(final Intent intent) {
if (intent == null) {
finish();
} else {
AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
@Override
protected byte[] doInBackground(final Void... params) {
String uri = intent.getStringExtra("uri");
if (uri != null) {
if (uri.startsWith("content://")) {
return getImageFromContentProvider(uri);
} else {
return getImageFromHttp(uri);
}
} else {
return intent.getByteArrayExtra("data");
}
}
@Override
protected void onPostExecute(final byte[] bytes) {
if (bytes != null) {
setImage(bytes);
} else {
finish();
}
}
};
task.execute();
}
}
private void setImage(final byte[] data) {
if (data == null) {
finish();
} else {
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (bitmap != null) {
ImageView view = (ImageView) findViewById(R.id.image_view);
view.setImageBitmap(bitmap);
}
}
}
private byte[] getImageFromContentProvider(final String uri) {
InputStream in = null;
try {
ContentResolver resolver = getContentResolver();
in = resolver.openInputStream(Uri.parse(uri));
return readBuffer(in);
} catch (IOException e) {
return null;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private byte[] getImageFromHttp(final String uri) {
InputStream in = null;
try {
URL url = new URL(uri);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000);
conn.setConnectTimeout(20000);
conn.setRequestMethod("GET");
conn.connect();
int resp = conn.getResponseCode();
if (resp == HttpURLConnection.HTTP_OK) {
in = conn.getInputStream();
return readBuffer(in);
} else {
return null;
}
} catch (IOException e) {
return null;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private byte[] readBuffer(final InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
return out.toByteArray();
}
}送られてきたIntentからURIを取得して、リソースを表示します。
MyServiceからCanvasを選択し、POST /gotapi/canvas/drawImage を実行します。
dataで選択したリソース画像が画面に表示されれば成功になります。