/*
DConnectMessageService.java
Copyright (c) 2014 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.message;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import org.deviceconnect.android.BuildConfig;
import org.deviceconnect.android.compat.AuthorizationRequestConverter;
import org.deviceconnect.android.compat.LowerCaseConverter;
import org.deviceconnect.android.compat.MessageConverter;
import org.deviceconnect.android.compat.ServiceDiscoveryRequestConverter;
import org.deviceconnect.android.event.Event;
import org.deviceconnect.android.event.EventManager;
import org.deviceconnect.android.event.cache.EventCacheController;
import org.deviceconnect.android.event.cache.MemoryCacheController;
import org.deviceconnect.android.localoauth.CheckAccessTokenResult;
import org.deviceconnect.android.localoauth.DevicePluginXmlProfile;
import org.deviceconnect.android.localoauth.DevicePluginXmlUtil;
import org.deviceconnect.android.localoauth.LocalOAuth2Main;
import org.deviceconnect.android.logger.AndroidHandler;
import org.deviceconnect.android.profile.AuthorizationProfile;
import org.deviceconnect.android.profile.DConnectProfile;
import org.deviceconnect.android.profile.DConnectProfileProvider;
import org.deviceconnect.android.profile.ServiceDiscoveryProfile;
import org.deviceconnect.android.profile.SystemProfile;
import org.deviceconnect.android.profile.spec.DConnectPluginSpec;
import org.deviceconnect.android.profile.spec.DConnectProfileSpec;
import org.deviceconnect.android.service.DConnectService;
import org.deviceconnect.android.service.DConnectServiceManager;
import org.deviceconnect.android.service.DConnectServiceProvider;
import org.deviceconnect.message.DConnectMessage;
import org.deviceconnect.message.intent.message.IntentDConnectMessage;
import org.deviceconnect.profile.AuthorizationProfileConstants;
import org.deviceconnect.profile.ServiceDiscoveryProfileConstants;
import org.deviceconnect.profile.SystemProfileConstants;
import org.json.JSONException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
/**
* Device Connectメッセージサービス.
*
* <p>
* Device Connectリクエストメッセージを受信し、Device Connectレスポンスメッセージを送信するサービスである。<br>
* {@link DConnectMessageServiceProvider}から呼び出されるサービスとし、UIレイヤーから明示的な呼び出しは行わない。
* </p>
*
* @author NTT DOCOMO, INC.
*/
public abstract class DConnectMessageService extends Service implements DConnectProfileProvider {
/**
* LocalOAuthで無視するプロファイル群.
*/
private static final String[] IGNORE_PROFILES = {
AuthorizationProfileConstants.PROFILE_NAME.toLowerCase(),
SystemProfileConstants.PROFILE_NAME.toLowerCase(),
ServiceDiscoveryProfileConstants.PROFILE_NAME.toLowerCase()
};
/** プロファイル仕様定義ファイルの拡張子. */
private static final String SPEC_FILE_EXTENSION = ".json";
/**
* ロガー.
*/
private Logger mLogger = Logger.getLogger("org.deviceconnect.dplugin");
/**
* プロファイルインスタンスマップ.
*/
private Map<String, DConnectProfile> mProfileMap = new HashMap<>();
/**
* Local OAuth使用フラグ.
* <p>
* デフォルトではtrueにしておくこと。
* </p>
*/
private boolean mUseLocalOAuth = true;
/**
* サービスを管理するクラス.
*/
private DConnectServiceProvider mServiceProvider;
/**
* リクエストを変換するコンバータクラス.
*/
private final MessageConverter[] mRequestConverters = {
new ServiceDiscoveryRequestConverter(),
new AuthorizationRequestConverter(),
new LowerCaseConverter()
};
/**
* プラグインのスペック.
*/
private DConnectPluginSpec mPluginSpec;
private final IBinder mLocalBinder = new LocalBinder();
private ScheduledExecutorService mExecutorService;
@Override
public void onCreate() {
super.onCreate();
setLogLevel();
EventManager.INSTANCE.setController(getEventCacheController());
mPluginSpec = loadPluginSpec();
DConnectServiceManager serviceManager = new DConnectServiceManager();
serviceManager.setPluginSpec(mPluginSpec);
serviceManager.setContext(getContext());
mServiceProvider = serviceManager;
mExecutorService = Executors.newSingleThreadScheduledExecutor();
// LocalOAuthの初期化
LocalOAuth2Main.initialize(this);
// 認証プロファイルの追加
addProfile(new AuthorizationProfile(this));
// 必須プロファイルの追加
addProfile(new ServiceDiscoveryProfile(mServiceProvider));
addProfile(getSystemProfile());
}
@Override
public void onDestroy() {
super.onDestroy();
// スレッドの停止
if (mExecutorService != null) {
mExecutorService.shutdown();
}
// LocalOAuthの後始末
LocalOAuth2Main.destroy();
}
@Override
public IBinder onBind(final Intent intent) {
return mLocalBinder;
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
super.onStartCommand(intent, flags, startId);
if (intent == null) {
mLogger.warning("request intent is null.");
return START_STICKY;
}
String action = intent.getAction();
if (action == null) {
mLogger.warning("request action is null. ");
return START_STICKY;
}
if (checkRequestAction(action)) {
convertRequest(intent);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
onRequest(intent, MessageUtils.createResponseIntent(intent));
}
});
}
if (checkManagerUninstall(intent)) {
onManagerUninstalled();
}
if (checkManagerTerminate(action)) {
onManagerTerminated();
}
if (checkManagerEventTransmitDisconnect(action)) {
onManagerEventTransmitDisconnected(intent.getStringExtra(IntentDConnectMessage.EXTRA_ORIGIN));
}
if (checkDevicePluginReset(action)) {
onDevicePluginReset();
}
return START_STICKY;
}
/**
* デバッグログの出力レベルを設定する.
* <p>
* デバッグフラグがfalseの場合には、ログを出力しないようにすること。
* </p>
*/
private void setLogLevel() {
if (BuildConfig.DEBUG) {
AndroidHandler handler = new AndroidHandler(mLogger.getName());
handler.setFormatter(new SimpleFormatter());
handler.setLevel(Level.ALL);
mLogger.addHandler(handler);
mLogger.setLevel(Level.ALL);
mLogger.setUseParentHandlers(false);
} else {
mLogger.setLevel(Level.OFF);
}
}
/**
* サービスを管理するクラスを取得する.
*
* @return サービス管理クラス
*/
public final DConnectServiceProvider getServiceProvider() {
return mServiceProvider;
}
protected final void setServiceProvider(final DConnectServiceProvider provider) {
mServiceProvider = provider;
}
protected final DConnectPluginSpec getPluginSpec() {
return mPluginSpec;
}
private DConnectPluginSpec loadPluginSpec() {
final Map<String, DevicePluginXmlProfile> supportedProfiles = DevicePluginXmlUtil.getSupportProfiles(this, getPackageName());
final AssetManager assets = getAssets();
final DConnectPluginSpec pluginSpec = new DConnectPluginSpec();
for (Map.Entry<String, DevicePluginXmlProfile> entry : supportedProfiles.entrySet()) {
String profileName = entry.getKey();
DevicePluginXmlProfile profile = entry.getValue();
try {
List<String> dirList = new ArrayList<>();
String assetsPath = profile.getSpecPath();
if (assetsPath != null) {
dirList.add(assetsPath);
}
dirList.add("api");
String filePath = null;
for (String dir : dirList) {
String[] fileNames = assets.list(dir);
String fileName = findProfileSpecName(fileNames, profileName);
if (fileName != null) {
filePath = dir + "/" + fileName;
break;
}
}
if (filePath == null) {
throw new RuntimeException("Profile spec is not found: " + profileName);
}
pluginSpec.addProfileSpec(profileName.toLowerCase(), assets.open(filePath));
mLogger.info("Loaded a profile spec: " + profileName);
} catch (IOException | JSONException e) {
throw new RuntimeException("Failed to load a profile spec: " + profileName, e);
}
}
return pluginSpec;
}
private static String findProfileSpecName(final String[] fileNames, final String profileName) {
if (fileNames == null) {
return null;
}
for (String fileFullName : fileNames) {
if (!fileFullName.endsWith(SPEC_FILE_EXTENSION)) {
continue;
}
String fileName = fileFullName.substring(0,
fileFullName.length() - SPEC_FILE_EXTENSION.length());
if (fileName.equalsIgnoreCase(profileName)) {
return fileFullName;
}
}
return null;
}
/**
* 指定されたアクションがDevice Connectのアクションかチェックします.
* @param action チェックするアクション
* @return Device Connectのアクションの場合はtrue、それ以外はfalse
*/
private boolean checkRequestAction(String action) {
return IntentDConnectMessage.ACTION_GET.equals(action)
|| IntentDConnectMessage.ACTION_POST.equals(action)
|| IntentDConnectMessage.ACTION_PUT.equals(action)
|| IntentDConnectMessage.ACTION_DELETE.equals(action);
}
/**
* リクエストのプロファイル名などを変換する.
*
* @param request 変換処理を行うリクエスト
*/
private void convertRequest(final Intent request) {
for (MessageConverter converter : mRequestConverters) {
converter.convert(request);
}
}
/**
* Device Connect Managerがアンインストールされたかをチェックします.
* @param intent intentパラメータ
* @return アンインストール時はtrue、それ以外はfalse
*/
private boolean checkManagerUninstall(final Intent intent) {
return Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intent.getAction()) &&
intent.getExtras().getBoolean(Intent.EXTRA_DATA_REMOVED) &&
intent.getDataString().contains("package:org.deviceconnect.android.manager");
}
/**
* Device Connect Manager 正常終了通知を受信したかをチェックします.
* @param action チェックするアクション
* @return Manager 正常終了検知でtrue、それ以外はfalse
*/
private boolean checkManagerTerminate(String action) {
return IntentDConnectMessage.ACTION_MANAGER_TERMINATED.equals(action);
}
/**
* Device Connect Manager のEvent 送信経路切断通知を受信したかチェックします.
* @param action チェックするアクション
* @return 検知受信でtrue、それ以外はfalse
*/
private boolean checkManagerEventTransmitDisconnect(String action) {
return IntentDConnectMessage.ACTION_EVENT_TRANSMIT_DISCONNECT.equals(action);
}
/**
* Device Plug-inへのReset要求を受信したかチェックします.
* @param action チェックするアクション
* @return Reset要求受信でtrue、それ以外はfalse
*/
private boolean checkDevicePluginReset(String action) {
return IntentDConnectMessage.ACTION_DEVICEPLUGIN_RESET.equals(action);
}
/**
* 受信したリクエストをプロファイルに振り分ける.
*
* @param request リクエストパラメータ
* @param response レスポンスパラメータ
*/
protected void onRequest(final Intent request, final Intent response) {
if (BuildConfig.DEBUG) {
mLogger.info("request: " + request);
mLogger.info("request extras: " + request.getExtras());
}
// プロファイル名の取得
String profileName = request.getStringExtra(DConnectMessage.EXTRA_PROFILE);
if (profileName == null) {
MessageUtils.setNotSupportProfileError(response);
sendResponse(response);
return;
}
boolean send = true;
if (isUseLocalOAuth()) {
// アクセストークン
String accessToken = request.getStringExtra(AuthorizationProfile.PARAM_ACCESS_TOKEN);
// LocalOAuth処理
CheckAccessTokenResult result = LocalOAuth2Main.checkAccessToken(accessToken, profileName,
IGNORE_PROFILES);
if (result.checkResult()) {
send = executeRequest(profileName, request, response);
} else {
if (accessToken == null) {
MessageUtils.setEmptyAccessTokenError(response);
} else if (!result.isExistAccessToken()) {
MessageUtils.setNotFoundClientId(response);
} else if (!result.isExistClientId()) {
MessageUtils.setNotFoundClientId(response);
} else if (!result.isExistScope()) {
MessageUtils.setScopeError(response);
} else if (!result.isNotExpired()) {
MessageUtils.setExpiredAccessTokenError(response);
} else {
MessageUtils.setAuthorizationError(response);
}
}
} else {
send = executeRequest(profileName, request, response);
}
if (send) {
sendResponse(response);
}
}
/**
* リクエストを実行する.
*
* @param profileName プロファイル名
* @param request リクエスト
* @param response レスポンス
* @return trueの場合には即座にレスポンスを返却する、それ以外の場合にはレスポンスを返却しない
*/
protected boolean executeRequest(final String profileName, final Intent request,
final Intent response) {
DConnectProfile profile = getProfile(profileName);
if (profile == null) {
String serviceId = DConnectProfile.getServiceID(request);
DConnectService service = getServiceProvider().getService(serviceId);
if (service != null) {
return service.onRequest(request, response);
} else {
MessageUtils.setNotFoundServiceError(response);
return true;
}
} else {
return profile.onRequest(request, response);
}
}
@Override
public List<DConnectProfile> getProfileList() {
return new ArrayList<>(mProfileMap.values());
}
@Override
public DConnectProfile getProfile(final String name) {
if (name == null) {
return null;
}
//XXXX パスの大文字小文字の無視
return mProfileMap.get(name.toLowerCase());
}
@Override
public void addProfile(final DConnectProfile profile) {
if (profile == null) {
return;
}
String profileName = profile.getProfileName().toLowerCase();
profile.setContext(this);
DConnectProfileSpec profileSpec = mPluginSpec.findProfileSpec(profileName);
if (profileSpec != null) {
profile.setProfileSpec(profileSpec);
}
//XXXX パスの大文字小文字の無視
mProfileMap.put(profileName, profile);
}
@Override
public void removeProfile(final DConnectProfile profile) {
if (profile == null) {
return;
}
//XXXX パスの大文字小文字の無視
mProfileMap.remove(profile.getProfileName().toLowerCase());
}
/**
* コンテキストの取得する.
*
* @return コンテキスト
*/
public final Context getContext() {
return this;
}
/**
* Device Connect Managerにレスポンスを返却するためのメソッド.
* @param response レスポンス
* @return 送信成功の場合true、それ以外はfalse
*/
public final boolean sendResponse(final Intent response) {
// TODO チェックが必要な追加すること。
if (response == null) {
throw new IllegalArgumentException("response is null.");
}
if (BuildConfig.DEBUG) {
mLogger.info("sendResponse: " + response);
mLogger.info("sendResponse Extra: " + response.getExtras());
}
getContext().sendBroadcast(response);
return true;
}
/**
* Device Connectにイベントを送信する.
*
* @param event イベントパラメータ
* @param accessToken 送り先のアクセストークン
* @return 送信成功の場合true、アクセストークンエラーの場合はfalseを返す。
*/
public final boolean sendEvent(final Intent event, final String accessToken) {
// TODO 返り値をもっと詳細なものにするか要検討
if (event == null) {
throw new IllegalArgumentException("Event is null.");
}
if (isUseLocalOAuth()) {
CheckAccessTokenResult result = LocalOAuth2Main.checkAccessToken(accessToken,
event.getStringExtra(DConnectMessage.EXTRA_PROFILE), IGNORE_PROFILES);
if (!result.checkResult()) {
return false;
}
}
if (BuildConfig.DEBUG) {
mLogger.info("sendEvent: " + event);
mLogger.info("sendEvent Extra: " + event.getExtras());
}
getContext().sendBroadcast(event);
return true;
}
/**
* Device Connectにイベントを送信する.
*
* @param event イベントパラメータ
* @param bundle パラメータ
* @return 送信成功の場合true、アクセストークンエラーの場合はfalseを返す。
*/
public final boolean sendEvent(final Event event, final Bundle bundle) {
Intent intent = EventManager.createEventMessage(event);
Bundle original = intent.getExtras();
original.putAll(bundle);
intent.putExtras(original);
return sendEvent(intent, event.getAccessToken());
}
/**
* Local OAuth使用フラグを設定する.
* <p>
* このフラグをfalseに設定することで、LocalOAuthの機能をOFFにすることができる。<br>
* デフォルトでは、trueになっているので、LocalOAuthが有効になっている。
* </p>
* @param use フラグ
*/
protected void setUseLocalOAuth(final boolean use) {
mUseLocalOAuth = use;
}
/**
* Local OAuth使用フラグを取得する.
*
* @return 使用する場合にはtrue、それ以外はfalse
*/
public boolean isUseLocalOAuth() {
return mUseLocalOAuth;
}
/**
* 指定されたプロファイルはLocal OAuth認証を無視して良いかを確認する.
*
* @param profileName プロファイル名
* @return 無視して良い場合はtrue、それ以外はfalse
*/
public boolean isIgnoredProfile(final String profileName) {
for (String name : IGNORE_PROFILES) {
if (name.equalsIgnoreCase(profileName)) { // MEMO パスの大文字小文字を無視
return true;
}
}
return false;
}
/**
* SystemProfileを取得する.
* <p>
* SystemProfileは必須実装となるため、本メソッドでSystemProfileのインスタンスを渡すこと。<br>
* このメソッドで返却したSystemProfileは自動で登録される。
* </p>
* @return SystemProfileのインスタンス
*/
protected abstract SystemProfile getSystemProfile();
/**
* EventCacheControllerのインスタンスを返す.
*
* <p>
* デフォルトではMemoryCacheControllerを使用する.<br>
* 変更したい場合は本メソッドをオーバーライドすること.
* </p>
*
* @return EventCacheControllerのインスタンス
*/
protected EventCacheController getEventCacheController() {
return new MemoryCacheController();
}
/**
* Device Connect Managerがアンインストールされた時に呼ばれる処理部.
* <p>
* Device Connect Managerがアンインストールされた場合に処理を行いたい場合には、
* このメソッドをオーバーライドして実装を行うこと。
* </p>
*/
protected void onManagerUninstalled() {
mLogger.info("SDK : onManagerUninstalled");
}
/**
* Device Connect Managerの正常終了通知を受信した時に呼ばれる処理部.
* <p>
* Device Connect Managerが終了された場合に処理を行い場合には、このメソッドをオーバーライドして実装を行うこと。
* </p>
*/
protected void onManagerTerminated() {
mLogger.info("SDK : on ManagerTerminated");
}
/**
* Device Connect ManagerのEvent送信経路切断通知を受信した時に呼ばれる処理部.
* <p>
* Device Connect ManagerでWebSocketなどが切断され、イベント停止要求が送られてきた場合には、
* このメソッドをオーバーライドして、イベントの停止処理や後始末の処理を行うこと。
* </p>
* @param origin イベント停止が要求されたオリジン
*/
protected void onManagerEventTransmitDisconnected(final String origin) {
mLogger.info("SDK : onManagerEventTransmitDisconnected: " + origin);
}
/**
* Device Plug-inへのReset要求を受信した時に呼ばれる処理部.
* <p>
* Device Connect Managerからデバイスプラグインのリセット要求が送られてきた場合には、
* このメソッドをオーバーライドして、再起動処理を行うこと。
* </p>
*/
protected void onDevicePluginReset() {
mLogger.info("SDK : onDevicePluginReset");
}
/**
* Serviceをバインドするためのクラス.
* <p>
* {@link org.deviceconnect.android.ui.activity.DConnectServiceListActivity}で、
* サービス一覧をを取得するためにバインドされる。
* </p>
*/
public class LocalBinder extends Binder {
/**
* DConnectMessageServiceのインスタンスを取得する.
* @return DConnectMessageServiceのインスタンス
*/
public DConnectMessageService getMessageService() {
return DConnectMessageService.this;
}
}
}