package org.deviceconnect.android.deviceplugin.alljoyn;
import android.app.Application;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log;
import org.alljoyn.about.AboutService;
import org.alljoyn.about.AboutServiceImpl;
import org.alljoyn.bus.BusAttachment;
import org.alljoyn.bus.Mutable;
import org.alljoyn.bus.OnPingListener;
import org.alljoyn.bus.ProxyBusObject;
import org.alljoyn.bus.SessionListener;
import org.alljoyn.bus.SessionOpts;
import org.alljoyn.bus.Status;
import org.alljoyn.bus.Variant;
import org.alljoyn.bus.alljoyn.DaemonInit;
import org.alljoyn.services.common.AnnouncementHandler;
import org.alljoyn.services.common.BusObjectDescription;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Application for AllJoyn.
* This class manages the global context of AllJoyn services.
*
* @author NTT DOCOMO, INC.
*/
public class AllJoynDeviceApplication extends Application {
private static final String SERVICE_NAME = "DConnectAllJoyn";
public static final String[] SINGLE_LAMP_INTERFACE_SET =
new String[]{
// AllJoyn Lighting service framework, Lamp service
"org.allseen.LSF.LampDetails"
, "org.allseen.LSF.LampParameters"
, "org.allseen.LSF.LampService"
, "org.allseen.LSF.LampState"
};
public static final String[] LAMP_CONTROLLER_INTERFACE_SET =
new String[]{
// // AllJoyn Lighting service framework, Controller Service
"org.allseen.LSF.ControllerService"
, "org.allseen.LSF.ControllerService.Lamp"
// , "org.allseen.LSF.ControllerService.LampGroup"
// , "org.allseen.LSF.ControllerService.Preset"
// , "org.allseen.LSF.ControllerService.Scene"
// , "org.allseen.LSF.ControllerService.MasterScene"
// , "org.allseen.LeaderElectionAndStateSync"
};
public static final String[][] SUPPORTED_INTERFACE_SETS = new String[][]{
SINGLE_LAMP_INTERFACE_SET
, LAMP_CONTROLLER_INTERFACE_SET
};
public static final int RESULT_OK = -1;
public static final int RESULT_FAILED = 0;
public static final String PARAM_RESULT_RECEIVER = "PARAM_RESULT_RECEIVER";
public static final String PARAM_BUS_NAME = "PARAM_BUS_NAME";
public static final String PARAM_PORT = "PARAM_PORT";
public static final String PARAM_SESSION_ID = "PARAM_SESSION_ID";
public static final int MSG_TYPE_INIT = 0;
public static final int MSG_TYPE_DISCOVER = 1;
public static final int MSG_TYPE_DESTROY = 2;
public static final int MSG_TYPE_JOIN_SESSION = 3;
public static final int MSG_TYPE_LEAVE_SESSION = 4;
public static final int MSG_TYPE_PING = 5;
static {
// Load AllJoyn native libraries.
System.loadLibrary("alljoyn_java");
}
private AllJoynHandler mAllJoynHandler;
// TODO: 到達不可のリモートバス(サービス)を削除する機構。
private Map<String, AllJoynServiceEntity> mAllJoynServiceEntities =
Collections.synchronizedMap(new LinkedHashMap<String, AllJoynServiceEntity>());
@Override
public void onCreate() {
super.onCreate();
startLightClient();
}
public void startLightClient() {
if (BuildConfig.DEBUG) {
Log.d(getClass().getSimpleName(), "startLightClient");
}
if (mAllJoynHandler == null) {
HandlerThread busThread = new HandlerThread("AllJoynHandler");
busThread.start();
mAllJoynHandler = new AllJoynHandler(busThread.getLooper());
}
final Message msg = new Message();
msg.what = MSG_TYPE_INIT;
Bundle data = new Bundle();
data.putParcelable(PARAM_RESULT_RECEIVER, new ResultReceiver() {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == RESULT_FAILED) {
if (BuildConfig.DEBUG) {
Log.w(AllJoynDeviceApplication.class.getSimpleName(),
"AllJoyn init failed, retrying...");
}
// Resend
mAllJoynHandler.postDelayed(new Runnable() {
@Override
public void run() {
mAllJoynHandler.sendMessage(msg);
}
}, 5000);
}
}
});
msg.obj = data;
mAllJoynHandler.sendMessage(msg);
}
public void performDiscovery() {
final Message msg = new Message();
msg.what = MSG_TYPE_DISCOVER;
Bundle data = new Bundle();
data.putParcelable(PARAM_RESULT_RECEIVER, new ResultReceiver() {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
}
});
msg.obj = data;
mAllJoynHandler.sendMessage(msg);
}
public Map<String, AllJoynServiceEntity> getDiscoveredAlljoynServices() {
return new LinkedHashMap<>(mAllJoynServiceEntities);
}
public void joinSession(@NonNull String busName, short port,
@NonNull ResultReceiver resultReceiver) {
final Message msg = new Message();
msg.what = MSG_TYPE_JOIN_SESSION;
Bundle data = new Bundle();
data.putString(PARAM_BUS_NAME, busName);
data.putShort(PARAM_PORT, port);
data.putParcelable(PARAM_RESULT_RECEIVER, resultReceiver);
msg.obj = data;
mAllJoynHandler.sendMessage(msg);
}
public void leaveSession(int sessionId, @NonNull ResultReceiver resultReceiver) {
final Message msg = new Message();
msg.what = MSG_TYPE_LEAVE_SESSION;
Bundle data = new Bundle();
data.putInt(PARAM_SESSION_ID, sessionId);
data.putParcelable(PARAM_RESULT_RECEIVER, resultReceiver);
msg.obj = data;
mAllJoynHandler.sendMessage(msg);
}
/**
* Obtain a proxy to an AllJoyn interface on a service.
* Through this proxy, properties, methods and signals of the service are accessed.
*
* @param busName service ID
* @param ifaceClass AllJoyn interface class
* @param <T> AllJoyn interface
* @return a concrete AllJoyn object
*/
public <T> T getInterface(@NonNull String busName, int sessionId, @NonNull Class<T> ifaceClass) {
AllJoynServiceEntity service = getServiceWithBusName(busName);
if (service == null || service.proxyObjects == null) {
return null;
}
// FIXME: Handling of multiple object paths with the specified interface.
// For the time being, use the first object path. Should these object paths be arranged
// as separate Device Connect services that can be accessed independently?
for (BusObjectDescription proxyObject : service.proxyObjects) {
for (String iface : proxyObject.interfaces) {
if (ifaceClass.getCanonicalName().equals(iface)) {
return mAllJoynHandler.getInterface(service.busName, proxyObject.path,
sessionId, ifaceClass);
}
}
}
return null;
}
public AllJoynServiceEntity getServiceWithBusName(String busName) {
for (AllJoynServiceEntity service : mAllJoynServiceEntities.values()) {
if (service.busName.equals(busName)) {
return service;
}
}
return null;
}
/**
* AllJoyn-related process handler.
* Access to AllJoyn SDK goes through an instance of this class for thread safety.
*/
// TODO: 非推奨のorg.alljoyn.about周辺のAPIから新しいorg.alljoyn.bus周辺のAPIへ移行する。
private class AllJoynHandler extends Handler implements AnnouncementHandler {
/**
* If an AllJoyn service is unresponsive for more than this duration, that service is
* removed from the discovered service list.
*/
private static final long ALIVE_TIMEOUT = 30000;
private static final int PING_TIMEOUT = 5000;
private static final int PING_INTERVAL = 10000;
private static final int DISCOVER_INTERVAL = 30000;
private BusAttachment mBus;
private AboutService mAboutService;
private ScheduledExecutorService mPingTimer;
private boolean mFirstTime = true;
private ScheduledExecutorService mDiscoverTimer;
public AllJoynHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.obj == null || !(msg.obj instanceof Bundle)) {
return;
}
Bundle data = (Bundle) msg.obj;
ResultReceiver resultReceiver = data.getParcelable(PARAM_RESULT_RECEIVER);
if (resultReceiver == null) {
return;
}
switch (msg.what) {
case MSG_TYPE_INIT:
doInitAllJoynContext(resultReceiver);
break;
case MSG_TYPE_DISCOVER:
doDiscover(resultReceiver);
break;
case MSG_TYPE_DESTROY:
doDestroyAllJoynContext(resultReceiver);
case MSG_TYPE_JOIN_SESSION: {
String sessionhostBusName = data.getString(PARAM_BUS_NAME);
if (sessionhostBusName == null) {
resultReceiver.send(RESULT_FAILED, null);
return;
}
if (!data.containsKey(PARAM_PORT)) {
resultReceiver.send(RESULT_FAILED, null);
return;
}
short sessionPort = data.getShort(PARAM_PORT);
doJoinSession(sessionhostBusName, sessionPort, resultReceiver);
break;
}
case MSG_TYPE_LEAVE_SESSION: {
if (!data.containsKey(PARAM_SESSION_ID)) {
resultReceiver.send(RESULT_FAILED, null);
return;
}
int sessionId = data.getInt(PARAM_SESSION_ID);
doLeaveSession(sessionId, resultReceiver);
break;
}
case MSG_TYPE_PING: {
String busName = data.getString(PARAM_BUS_NAME);
if (busName == null) {
resultReceiver.send(RESULT_FAILED, null);
return;
}
doPing(busName, resultReceiver);
break;
}
}
}
private boolean isSupported(@NonNull BusObjectDescription[] busObjects) {
List<String> interfaces = new LinkedList<>();
for (BusObjectDescription busObject : busObjects) {
Collections.addAll(interfaces, busObject.interfaces);
}
// Each supported AllJoyn interface set represents a collection of required AllJoyn
// interfaces to realize a certain DeviceConnect profile (e.g. AllJoyn Lamp service
// interfaces are required for the DeviceConnect Light profile).
// If AllJoyn bus object in question contains any of supported interface sets, then
// assumedly this bus object is able to become a DeviceConect service.
for (String[] supportedInterfaceSet : SUPPORTED_INTERFACE_SETS) {
if (interfaces.containsAll(Arrays.asList(supportedInterfaceSet))) {
return true;
}
}
return false;
}
@Override
public void onAnnouncement(String busName, short port,
BusObjectDescription[] busObjects,
Map<String, Variant> aboutMap) {
AllJoynServiceEntity service =
new AllJoynServiceEntity(busName, port, aboutMap, busObjects);
if (BuildConfig.DEBUG) {
Log.i(AllJoynHandler.this.getClass().getSimpleName(),
"Service found: " + service.serviceName);
}
if (!isSupported(busObjects)) {
if (BuildConfig.DEBUG) {
Log.i(AllJoynHandler.this.getClass().getSimpleName(),
"Required I/Fs are missing. Ignoring \"" + service.serviceName + "\"");
}
return;
}
putEntity(service);
}
@Override
public void onDeviceLost(String busName) {
// Remove the service
if (BuildConfig.DEBUG) {
Log.d(AllJoynHandler.this.getClass().getSimpleName(), "onDeviceLost received.");
}
AllJoynServiceEntity service = getServiceWithBusName(busName);
if (service != null) {
if (BuildConfig.DEBUG) {
Log.i(AllJoynHandler.this.getClass().getSimpleName(),
"Service " + service.serviceName + " is removed.");
}
removeEntity(service);
}
}
/**
* Obtain a proxy to an AllJoyn interface on a service.
* Through this proxy, properties, methods and signals of the service are accessed.
*
* @param busName messaging bus name
* @param objPath object path
* @param sessionId session ID
* @param ifaceClass AllJoyn interface class
* @param <T> AllJoyn interface
* @return a concrete AllJoyn object
*/
public <T> T getInterface(@NonNull String busName, @NonNull String objPath,
int sessionId, @NonNull Class<T> ifaceClass) {
ProxyBusObject proxyObj =
mBus.getProxyBusObject(busName, objPath, sessionId, new Class[]{ifaceClass});
if (proxyObj == null) {
return null;
}
return proxyObj.getInterface(ifaceClass);
}
/**
* Initialize an AllJoyn context (message bus).
*
* @param resultReceiver
*/
private void doInitAllJoynContext(@NonNull ResultReceiver resultReceiver) {
if (BuildConfig.DEBUG) {
Log.d(AllJoynHandler.this.getClass().getSimpleName(), "init");
}
if (mBus == null) {
DaemonInit.PrepareDaemon(getApplicationContext());
mBus = new BusAttachment(getPackageName(), BusAttachment.RemoteMessage.Receive);
Status status = mBus.connect();
if (status != Status.OK) {
Log.e(this.getClass().getSimpleName(), "Failed to connect to a bus.");
mBus = null;
resultReceiver.send(RESULT_FAILED, null);
return;
}
try {
mAboutService = AboutServiceImpl.getInstance();
mAboutService.startAboutClient(mBus);
mAboutService.addAnnouncementHandler(this, null);
} catch (Exception e) {
Log.e(this.getClass().getSimpleName(), "Failed to start About client.");
mBus = null;
resultReceiver.send(RESULT_FAILED, null);
return;
}
mPingTimer = Executors.newScheduledThreadPool(3);
final ResultReceiver pingResultReceiver = new ResultReceiver() {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultData == null || !resultData.containsKey(PARAM_BUS_NAME)) {
Log.e(AllJoynHandler.this.getClass().getSimpleName(),
"Logical error: ping result must be present.");
return;
}
String busName = resultData.getString(PARAM_BUS_NAME);
AllJoynServiceEntity service = getServiceWithBusName(busName);
if (service != null) {
if (resultCode != RESULT_OK) {
if (new Date().getTime() - service.lastAlive.getTime() > ALIVE_TIMEOUT) {
if (BuildConfig.DEBUG) {
Log.i(AllJoynHandler.this.getClass().getSimpleName(),
"Ping failed: " + service.serviceName +
". Removing it from discovered services...");
}
removeEntity(service);
}
} else {
if (BuildConfig.DEBUG) {
Log.i(AllJoynHandler.this.getClass().getSimpleName(),
"Ping succeeded: " + service.serviceName + ".");
}
service.lastAlive = new Date();
}
}
}
};
mPingTimer.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (BuildConfig.DEBUG) {
Log.i(AllJoynHandler.this.getClass().getSimpleName(),
"Sending pings to discovered services...");
}
for (AllJoynServiceEntity serviceEntity : mAllJoynServiceEntities.values()) {
Message msg = new Message();
msg.what = MSG_TYPE_PING;
Bundle data = new Bundle();
data.putString(PARAM_BUS_NAME, serviceEntity.busName);
data.putParcelable(PARAM_RESULT_RECEIVER, pingResultReceiver);
msg.obj = data;
AllJoynHandler.this.sendMessage(msg);
}
}
}, 0, PING_INTERVAL, TimeUnit.MILLISECONDS);
mDiscoverTimer = Executors.newSingleThreadScheduledExecutor();
mDiscoverTimer.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
performDiscovery();
}
}, 0, DISCOVER_INTERVAL, TimeUnit.MILLISECONDS);
}
resultReceiver.send(RESULT_OK, null);
}
/**
* @param resultReceiver
*/
private void doDestroyAllJoynContext(@NonNull ResultReceiver resultReceiver) {
try {
if (mPingTimer != null) {
mPingTimer.shutdownNow();
mPingTimer = null;
}
if (mDiscoverTimer != null) {
mDiscoverTimer.shutdownNow();
mDiscoverTimer = null;
}
if (mAboutService != null) {
mAboutService.stopAboutClient();
mAboutService = null;
}
if (mBus != null) {
mBus.disconnect();
mBus = null;
}
} catch (Exception e) {
Log.e(AllJoynHandler.this.getClass().getSimpleName(),
"Failed to destroy AllJoyn context.");
resultReceiver.send(RESULT_FAILED, null);
return;
}
resultReceiver.send(RESULT_OK, null);
}
/**
* Discover AllJoyn services with DeviceConnect-compatible interfaces.
*
* @param resultReceiver
*/
private void doDiscover(@NonNull ResultReceiver resultReceiver) {
if (BuildConfig.DEBUG) {
Log.d(AllJoynHandler.this.getClass().getSimpleName(), "discover");
}
// NOTE: the effect of whoImplements() for specific interfaces can stack up, and unless
// all stacked-up effects are canceled, service discovery for the specific interfaces
// can not be re-performed.
// So the number of calls for whoImplements() and cancelWhoImplements() must
// be balanced.
if (!mFirstTime) {
for (String[] ifaceSet : SUPPORTED_INTERFACE_SETS) {
for (String iface : ifaceSet) {
mBus.cancelWhoImplements(new String[]{iface});
}
}
} else {
mFirstTime = false;
}
// To realize fine-grained API availability for DeviceConnect,
// query each AllJoyn interface separately.
for (String[] ifaceSet : SUPPORTED_INTERFACE_SETS) {
for (String iface : ifaceSet) {
mBus.whoImplements(new String[]{iface});
}
}
}
/**
* Join a messaging session hosted by a service.
* Messaging session is specified by its hosting bus name and port.
*
* @param sessionHostBusName
* @param sessionPort
* @param resultReceiver
*/
private void doJoinSession(@NonNull String sessionHostBusName, short sessionPort,
@NonNull ResultReceiver resultReceiver) {
if (BuildConfig.DEBUG) {
Log.d(AllJoynHandler.this.getClass().getSimpleName(), "joinSession");
}
SessionOpts sessionOpts = new SessionOpts();
Mutable.IntegerValue sessionId = new Mutable.IntegerValue();
Status status = mBus.joinSession(sessionHostBusName, sessionPort,
sessionId, sessionOpts, new SessionListener() {
@Override
public void sessionLost(int sessionId, int reason) {
}
@Override
public void sessionMemberAdded(int sessionId, String uniqueName) {
}
@Override
public void sessionMemberRemoved(int sessionId, String uniqueName) {
}
});
if (status == Status.OK) {
Bundle resultData = new Bundle();
resultData.putInt(PARAM_SESSION_ID, sessionId.value);
resultReceiver.send(RESULT_OK, resultData);
} else {
resultReceiver.send(RESULT_FAILED, null);
}
}
private void doLeaveSession(int sessionId, @NonNull ResultReceiver resultReceiver) {
if (BuildConfig.DEBUG) {
Log.d(AllJoynHandler.this.getClass().getSimpleName(), "leaveSession");
}
Status status = mBus.leaveSession(sessionId);
if (status == Status.OK) {
resultReceiver.send(RESULT_OK, null);
} else {
resultReceiver.send(RESULT_FAILED, null);
}
}
private void doPing(final String busName, @NonNull final ResultReceiver resultReceiver) {
if (BuildConfig.DEBUG) {
Log.d(AllJoynHandler.this.getClass().getSimpleName(),
"ping the service with bus name \"" + busName + "\"");
}
final AtomicBoolean finished = new AtomicBoolean(false);
final Bundle data = new Bundle();
data.putString(PARAM_BUS_NAME, busName);
Status pingStatus = mBus.ping(busName, PING_TIMEOUT, new OnPingListener() {
@Override
public void onPing(Status status, Object context) {
synchronized (finished) {
if (!finished.get()) {
if (status == Status.OK) {
resultReceiver.send(RESULT_OK, data);
} else {
resultReceiver.send(RESULT_FAILED, data);
}
finished.set(true);
}
}
}
}, null);
if (pingStatus != Status.OK) {
synchronized (finished) {
if (!finished.get()) {
resultReceiver.send(RESULT_FAILED, data);
finished.set(true);
}
}
}
}
}
public class ResultReceiver extends android.os.ResultReceiver {
public ResultReceiver() {
super(mAllJoynHandler);
}
}
private void removeEntity(final AllJoynServiceEntity entity) {
if (mAllJoynServiceEntities.remove(entity.appId) != null) {
notifyOnDisconnect(entity);
}
}
private void putEntity(final AllJoynServiceEntity entity) {
if (mAllJoynServiceEntities.containsKey(entity.appId)) {
AllJoynServiceEntity oldService = mAllJoynServiceEntities.get(entity.appId);
entity.lastAlive = oldService.lastAlive;
}
mAllJoynServiceEntities.put(entity.appId, entity);
notifyOnConnect(entity);
}
public interface ConnectionListener {
void onConnect(AllJoynServiceEntity entity);
void onDisconnect(AllJoynServiceEntity entity);
}
private ConnectionListener mConnectionListener;
public void setConnectionListener(final ConnectionListener listener) {
mConnectionListener = listener;
}
private void notifyOnConnect(AllJoynServiceEntity entity) {
if (mConnectionListener != null) {
mConnectionListener.onConnect(entity);
}
}
private void notifyOnDisconnect(AllJoynServiceEntity entity) {
if (mConnectionListener != null) {
mConnectionListener.onDisconnect(entity);
}
}
}