/*
HvcDeviceService.java
Copyright (c) 2015 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.deviceplugin.hvc;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import org.deviceconnect.android.deviceplugin.hvc.ble.BleDeviceDetector;
import org.deviceconnect.android.deviceplugin.hvc.ble.BleDeviceDetector.BleDeviceDiscoveryListener;
import org.deviceconnect.android.deviceplugin.hvc.comm.HvcCommManager;
import org.deviceconnect.android.deviceplugin.hvc.comm.HvcCommManagerUtils;
import org.deviceconnect.android.deviceplugin.hvc.humandetect.HumanDetectKind;
import org.deviceconnect.android.deviceplugin.hvc.humandetect.HumanDetectRequestParams;
import org.deviceconnect.android.deviceplugin.hvc.profile.HvcConstants;
import org.deviceconnect.android.deviceplugin.hvc.profile.HvcHumanDetectionProfile;
import org.deviceconnect.android.deviceplugin.hvc.profile.HvcServiceDiscoveryProfile;
import org.deviceconnect.android.deviceplugin.hvc.profile.HvcSystemProfile;
import org.deviceconnect.android.deviceplugin.hvc.service.HvcService;
import org.deviceconnect.android.message.DConnectMessageService;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.android.profile.SystemProfile;
import org.deviceconnect.android.service.DConnectService;
import org.deviceconnect.message.DConnectMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Pattern;
/**
* HVC Device Service.
*
* @author NTT DOCOMO, INC.
*/
public class HvcDeviceService extends DConnectMessageService {
/**
* log tag.
*/
private static final String TAG = HvcDeviceService.class.getSimpleName();
/**
* Debug.
*/
private static final Boolean DEBUG = BuildConfig.DEBUG;
/**
* HVC comm managers(1serviceId,1record).
*/
private final List<HvcCommManager> mHvcCommManagerArray = new ArrayList<>();
/**
* event interval timer information array.<br>
* - not exist record : stop timer.<br>
* - exist record : running timer.<br>
*/
private List<HvcTimerInfo> mIntervalTimerInfoArray = new ArrayList<>();
/**
* HVC found device list.
*/
private final List<BluetoothDevice> mCacheDeviceList = new ArrayList<>();
/**
* BLE device detector.
*/
private BleDeviceDetector mDetector;
@Override
public void onCreate() {
super.onCreate();
// start HVC device search.
startSearchHvcDevice();
// add supported profiles
addProfile(new HvcServiceDiscoveryProfile(getServiceProvider()));
addProfile(new HvcHumanDetectionProfile());
// start timeout judge timer.
startTimeoutJudgeTimer();
}
@Override
protected void onManagerUninstalled() {
// Managerアンインストール検知時の処理。
if (DEBUG) {
Log.i(TAG, "Plug-in : onManagerUninstalled");
}
resetPluginResource();
}
@Override
protected void onManagerTerminated() {
// Manager正常終了通知受信時の処理。
if (DEBUG) {
Log.i(TAG, "Plug-in : onManagerTerminated");
}
}
@Override
protected void onManagerEventTransmitDisconnected(String origin) {
// ManagerのEvent送信経路切断通知受信時の処理。
if (DEBUG) {
Log.i(TAG, "Plug-in : onManagerEventTransmitDisconnected");
}
if (origin != null) {
unregisterDetectionEventByMatchedOrigin(origin);
} else {
removeAllDetectEvent();
}
}
@Override
protected void onDevicePluginReset() {
// Device Plug-inへのReset要求受信時の処理。
if (DEBUG) {
Log.i(TAG, "Plug-in : onDevicePluginReset");
}
resetPluginResource();
}
/**
* リソースリセット処理.
*/
private void resetPluginResource() {
/** 全イベント削除. */
removeAllDetectEvent();
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null) {
return START_STICKY;
}
String action = intent.getAction();
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
if (DEBUG) {
Log.d(TAG, "bluetooth state change.");
}
Bundle extras = intent.getExtras();
int state = extras.getInt(BluetoothAdapter.EXTRA_STATE);
if (state == BluetoothAdapter.STATE_TURNING_OFF) {
// Bluetooth ON -> OFF
// stop scan process.
stopSearchHvcDevice();
turnOff(getServiceProvider().getServiceList());
} else if (state == BluetoothAdapter.STATE_ON) {
// Bluetooth OFF -> ON
// start scan process.
startSearchHvcDevice();
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
protected SystemProfile getSystemProfile() {
return new HvcSystemProfile();
}
//
// service discovery profile
//
/**
* get HVC device list.
*
* @return HVC device list
*/
public List<BluetoothDevice> getHvcDeviceList() {
List<BluetoothDevice> deviceList = new ArrayList<>();
synchronized (mCacheDeviceList) {
deviceList.addAll(mCacheDeviceList);
}
synchronized (mHvcCommManagerArray) {
deviceList.addAll(HvcCommManagerUtils.getConnectedBluetoothDevices(mHvcCommManagerArray));
}
removeDuplicateOrNonHvcData(deviceList);
return deviceList;
}
//
// for human detect profile
//
//
// register detection event.
//
/**
* Human Detect Profile register detection event.
*
* @param detectKind detectKind
* @param requestParams request parameters.
* @param response response
* @param serviceId serviceId
* @param origin origin
*/
public void registerDetectionEvent(final HumanDetectKind detectKind, final HumanDetectRequestParams requestParams,
final Intent response, final String serviceId, final String origin) {
if (DEBUG) {
Log.d(TAG, "registerDetectionEvent(). detectKind:" + detectKind.toString() + " serviceId:" + serviceId
+ " origin:" + origin);
}
// Bluetooth OFF
if (mDetector == null || !mDetector.isEnabled()) {
if (DEBUG) {
Log.d(TAG, "Bluetooth OFF");
}
MessageUtils.setIllegalDeviceStateError(response, "bluetooth OFF.");
sendResponse(response);
return;
}
// search CommManager by serviceId(if not found, add CommManager.).
HvcCommManager commManager;
synchronized (mHvcCommManagerArray) {
commManager = HvcCommManagerUtils.search(mHvcCommManagerArray, serviceId);
}
if (commManager == null) {
// search cache bluetooth device.(if not found, not found service error)
BluetoothDevice bluetoothDevice = searchCacheHvcDevice(serviceId);
if (bluetoothDevice == null) {
MessageUtils.setNotFoundServiceError(response);
sendResponse(response);
return;
}
// add comm manager.
commManager = new HvcCommManager(this, serviceId, bluetoothDevice);
synchronized (mHvcCommManagerArray) {
mHvcCommManagerArray.add(commManager);
}
}
// start interval timer. (if no event with same interval.)
startIntervalTimer(requestParams.getEvent().getInterval());
// add event data to commManager.
commManager.registerDetectEvent(detectKind, requestParams, response, origin);
}
//
// unregister detection event.
//
/**
* Human Detect Profile unregister detection event.<br>
*
* @param detectKind detectKind
* @param response response
* @param serviceId serviceId
* @param origin origin
*/
public void unregisterDetectionEvent(final HumanDetectKind detectKind, final Intent response,
final String serviceId, final String origin) {
if (DEBUG) {
Log.d(TAG, "unregisterDetectionEvent(). detectKind:" + detectKind.toString() + " serviceId:" + serviceId
+ " origin:" + origin);
}
// search CommManager by serviceId.
HvcCommManager commManager;
synchronized (mHvcCommManagerArray) {
commManager = HvcCommManagerUtils.search(mHvcCommManagerArray, serviceId);
}
if (commManager == null) {
MessageUtils.setNotFoundServiceError(response, "service id not found");
sendResponse(response);
return;
}
// get event interval.
Long interval = commManager.getEventInterval(detectKind, origin);
if (interval == null) {
MessageUtils.setInvalidRequestParameterError(response, "detectKind and origin pair not found.");
sendResponse(response);
return;
}
// unregister
commManager.unregisterDetectEvent(detectKind, origin);
// if no event with same interval, stop interval timer (and remove
// interval timer info record).
boolean result = HvcCommManagerUtils.checkExistEventByInterval(mHvcCommManagerArray, interval);
if (!result) {
if (DEBUG) {
Log.d(TAG, "stop interval timer. interval:" + interval);
}
stopIntervalTimer(interval);
}
response.putExtra(DConnectMessage.EXTRA_RESULT, DConnectMessage.RESULT_OK);
response.putExtra(DConnectMessage.EXTRA_VALUE, "Unregister OnDetection event");
sendResponse(response);
}
/**
* Human Detect Profile unregister detection event by matched origin.
* @param origin origin
*/
private void unregisterDetectionEventByMatchedOrigin(final String origin) {
for (HvcCommManager commManager : mHvcCommManagerArray) {
commManager.removeDetectEvent(origin);
HumanDetectKind kind;
for (int i = 0; i < 3; i++) {
switch (i) {
case 0:
kind = HumanDetectKind.BODY;
break;
case 1:
kind = HumanDetectKind.FACE;
break;
case 2:
default:
kind = HumanDetectKind.HAND;
break;
}
Long interval = commManager.getEventInterval(kind, origin);
if (interval != null) {
stopIntervalTimer(interval);
}
}
}
}
/**
* remove all detect event.
*/
private void removeAllDetectEvent() {
for (HvcCommManager commManager : mHvcCommManagerArray) {
/** 全イベント解除 */
commManager.removeAllDetectEvent();
/** 全インターバルタイマー削除 */
int count = mIntervalTimerInfoArray.size();
for (int index = (count - 1); index >= 0; index--) {
HvcTimerInfo timerInfo = mIntervalTimerInfoArray.get(index);
timerInfo.stopTimer();
mIntervalTimerInfoArray.remove(index);
}
}
}
/**
* Human Detect Profile get detection.<br>
*
* @param detectKind detectKind
* @param requestParams request parameters.
* @param response response
* @param serviceId serviceId
*/
public void doGetDetectionProc(final HumanDetectKind detectKind, final HumanDetectRequestParams requestParams,
final Intent response, final String serviceId) {
if (DEBUG) {
Log.d(TAG, "doGetDetectionProc(). detectKind:" + detectKind.toString() + " serviceId:" + serviceId);
}
// Bluetooth OFF
if (mDetector == null || !mDetector.isEnabled()) {
MessageUtils.setIllegalDeviceStateError(response, "bluetooth OFF.");
sendResponse(response);
if (DEBUG) {
Log.d(TAG, "Bluetooth OFF");
}
return;
}
// search CommManager by serviceId(if not found, add CommManager.).
HvcCommManager commManager;
synchronized (mHvcCommManagerArray) {
commManager = HvcCommManagerUtils.search(mHvcCommManagerArray, serviceId);
}
if (commManager == null) {
// search cache bluetooth device.(if not found, not found service error)
BluetoothDevice bluetoothDevice = searchCacheHvcDevice(serviceId);
if (bluetoothDevice == null) {
MessageUtils.setNotFoundServiceError(response);
sendResponse(response);
if (DEBUG) {
Log.d(TAG, "service not found");
}
return;
}
// add comm manager.
commManager = new HvcCommManager(this, serviceId, bluetoothDevice);
synchronized (mHvcCommManagerArray) {
mHvcCommManagerArray.add(commManager);
}
}
final HvcCommManager commManagerFinal = commManager;
// get detection process. (if in communication, wait and retry)
retryProcInNewThread(HvcConstants.HVC_COMM_RETRY_COUNT, HvcConstants.HVC_COMM_RETRY_INTERVAL,
new HvcRetryProcListener() {
@Override
public boolean judgeRetry() {
if (DEBUG) {
Log.d(TAG, "judgeRetry()");
}
// retry(Now in the device communication)
if (commManagerFinal.checkCommBusy()) {
if (DEBUG) {
Log.d(TAG, "retry");
}
return true;
}
// no retry
if (DEBUG) {
Log.d(TAG, "no retry");
}
return false;
}
@Override
public void procOnNewThread() {
if (DEBUG) {
Log.d(TAG, "proc()");
}
// get detection process.
commManagerFinal.doGetDetectionProc(detectKind, requestParams, response);
}
@Override
public void timeoutProc() {
if (DEBUG) {
Log.d(TAG, "timeoutProc()");
}
// timeout error.
MessageUtils.setTimeoutError(response, "GET API timeout.");
sendResponse(response);
}
});
}
/**
* Initialize BLE device detector.
*/
private void initDetector() {
if (mDetector == null) {
mDetector = new BleDeviceDetector(getContext());
mDetector.setListener(new BleDeviceDiscoveryListener() {
@Override
public void onDiscovery(final List<BluetoothDevice> currentDevices) {
// remove duplicate data or non HVC data.
removeDuplicateOrNonHvcData(currentDevices);
synchronized (mCacheDeviceList) {
mCacheDeviceList.clear();
mCacheDeviceList.addAll(currentDevices);
}
turnOn(currentDevices);
turnOff(findLostServices(currentDevices));
}
});
}
}
private List<DConnectService> findLostServices(final List<BluetoothDevice> currentList) {
List<DConnectService> lostServices = new ArrayList<DConnectService>();
for (DConnectService cached : getServiceProvider().getServiceList()) {
boolean isFound = false;
check:
for (BluetoothDevice current : currentList) {
if (cached.getId().equals(HvcService.createServiceId(current))) {
isFound = true;
break check;
}
}
if (!isFound) {
lostServices.add(cached);
}
}
return lostServices;
}
private void turnOn(final List<BluetoothDevice> devices) {
for (BluetoothDevice device : devices) {
DConnectService service = getServiceProvider().getService(device.getAddress());
if (service == null) {
service = new HvcService(device);
getServiceProvider().addService(service);
}
service.setOnline(true);
}
}
private void turnOff(final List<DConnectService> services) {
for (DConnectService service : services) {
service.setOnline(false);
}
}
/**
* retry process in new thread.
*
* @param retryCount retry count
* @param retryInteval retry interval[msec]
* @param listener retry process listener.
*/
private void retryProcInNewThread(final int retryCount, final int retryInteval,
final HvcRetryProcListener listener) {
if (DEBUG) {
Log.d(TAG, "retryProcOnNewThread() start");
}
// new thread start
Thread thread = new Thread() {
@Override
public void run() {
if (DEBUG) {
Log.d(TAG, "retryProcOnNewThread() - run() start");
}
// retry loop
for (int index = 0; index < retryCount; index++) {
// judge retry.
if (!listener.judgeRetry()) {
// if no retry, call process.
listener.procOnNewThread();
return;
}
// if retry, interval sleep and next loop.
if (DEBUG) {
Log.d(TAG, "retryProcOnNewThread() - Retry");
}
try {
sleep(retryInteval);
} catch (InterruptedException e) {
if (DEBUG) {
Log.d(TAG, "retryProcOnNewThread() - run() sleep exception, " + e.getMessage());
}
}
}
// timeout process.
listener.timeoutProc();
}
};
thread.start();
}
/**
* retry process listener.
*/
private interface HvcRetryProcListener {
/**
* judge retry.
*
* @return true: retry / false: no retry
*/
boolean judgeRetry();
/**
* process on new thread.
*/
void procOnNewThread();
/**
* timeout process.
*/
void timeoutProc();
}
/**
* search cache HVC device by serviceId.
*
* @param serviceId serviceId
* @return not null: found device / null: not found
*/
public BluetoothDevice searchCacheHvcDevice(final String serviceId) {
List<BluetoothDevice> hvcDeviceList = getHvcDeviceList();
for (BluetoothDevice device : hvcDeviceList) {
if (serviceId.equals(HvcCommManager.getServiceId(device.getAddress()))) {
return device;
}
}
return null;
}
public synchronized void startSearchHvcDevice() {
if (mDetector == null) {
initDetector();
}
HvcDeviceApplication.getInstance().checkLocationEnable();
if (mDetector.isEnabled()) {
mDetector.startScan();
}
}
public synchronized void stopSearchHvcDevice() {
mDetector.stopScan();
}
/**
* start interval timer(if no event with same interval.).
*
* @param interval interval[msec]
*/
private void startIntervalTimer(final long interval) {
// search timer, if interval to match.
HvcTimerInfo timerInfo = HvcTimerInfoUtils.search(mIntervalTimerInfoArray, interval);
if (timerInfo == null) {
// if no match, start interval timer.
timerInfo = new HvcTimerInfo(interval);
timerInfo.startTimer(new TimerTask() {
@Override
public void run() {
if (DEBUG) {
Log.d(TAG, "event interval timer proc.");
}
// Bluetooth OFF
if (mDetector == null || !mDetector.isEnabled()) {
if (DEBUG) {
Log.d(TAG, "can not send event. (bluetooth OFF)");
}
return;
}
// call event process.
synchronized (mHvcCommManagerArray) {
for (HvcCommManager commManager : mHvcCommManagerArray) {
commManager.onEventProc(interval);
}
}
}
});
mIntervalTimerInfoArray.add(timerInfo);
}
}
/**
* stop interval timer(and remove record).
*
* @param interval interval
*/
private void stopIntervalTimer(final long interval) {
int count = mIntervalTimerInfoArray.size();
for (int index = (count - 1); index >= 0; index--) {
HvcTimerInfo timerInfo = mIntervalTimerInfoArray.get(index);
if (timerInfo.getInterval() == interval) {
timerInfo.stopTimer();
mIntervalTimerInfoArray.remove(index);
}
}
}
/**
* start timeout judge timer.
*/
private void startTimeoutJudgeTimer() {
// timeout judge timer information.
HvcTimerInfo mTimeoutJudgeTimer = new HvcTimerInfo(HvcConstants.TIMEOUT_JUDGE_INTERVAL);
mTimeoutJudgeTimer.startTimer(new TimerTask() {
@Override
public void run() {
if (DEBUG) {
Log.d(TAG, "timeout judge timer proc.");
}
// call timeout judge process.
synchronized (mHvcCommManagerArray) {
for (HvcCommManager commManager : mHvcCommManagerArray) {
commManager.onTimeoutJudgeProc();
}
}
}
});
}
/**
* remove duplicate data.
* @param devices found devices.
*/
private void removeDuplicateOrNonHvcData(final List<BluetoothDevice> devices) {
Pattern p = Pattern.compile(HvcConstants.HVC_DEVICE_NAME_PREFIX);
int deviceCount = devices.size();
for (int deviceIndex = (deviceCount - 1); deviceIndex >= 0; deviceIndex--) {
// remove if non HVC device name.
if (devices.get(deviceIndex) == null
|| devices.get(deviceIndex).getName() == null
|| !p.matcher(devices.get(deviceIndex).getName()).find()) {
devices.remove(deviceIndex);
continue;
}
// remove if duplicate.
boolean isFoundDuplicate = false;
for (int compareIndex = 0; compareIndex < deviceIndex; compareIndex++) {
if (devices.get(deviceIndex).equals(devices.get(compareIndex))) {
isFoundDuplicate = true;
break;
}
}
if (isFoundDuplicate) {
devices.remove(deviceIndex);
}
}
}
/**
* timer information.
*
* @author NTT DOCOMO, INC.
*/
private class HvcTimerInfo {
/**
* Interval[msec].
*/
private long mInterval;
/**
* Timer.
*/
private Timer mTimer;
/**
* Timer running flag.
*/
private boolean mIsTimerRunning;
/**
* Constructor.
*
* @param interval interval[msec]
*/
public HvcTimerInfo(final long interval) {
mInterval = interval;
mTimer = new Timer();
mIsTimerRunning = false;
}
/**
* get interval.
*
* @return interval[msec]
*/
public long getInterval() {
return mInterval;
}
/**
* start timer.
*
* @param intervalTimerTask interval timer task
*/
public void startTimer(final TimerTask intervalTimerTask) {
if (!mIsTimerRunning) {
// add timertask.
mTimer.scheduleAtFixedRate(intervalTimerTask, 0, mInterval);
mIsTimerRunning = true;
} else {
// change timertask.
mTimer.cancel();
mTimer.scheduleAtFixedRate(intervalTimerTask, 0, mInterval);
}
}
/**
* stop timer.
*/
public void stopTimer() {
if (mIsTimerRunning) {
mTimer.cancel();
}
}
}
/**
* timer information utility.
*
* @author NTT DOCOMO, INC.
*/
private static class HvcTimerInfoUtils {
/**
* search data by interval.
*
* @param hvcTimerInfoArray array
* @param interval interval(search key)
* @return not null: found data. / null: not found.
*/
public static HvcTimerInfo search(final List<HvcTimerInfo> hvcTimerInfoArray, final long interval) {
for (HvcTimerInfo hvcTimerInfo : hvcTimerInfoArray) {
if (hvcTimerInfo.getInterval() == interval) {
return hvcTimerInfo;
}
}
return null;
}
}
}