From 0b67c0f74acdbf0dece463d2c875bd64106ffb7d Mon Sep 17 00:00:00 2001 From: ayyoob Date: Sun, 17 Apr 2016 12:18:25 +0530 Subject: [PATCH] added android sense agent code --- .gitignore | 7 + .../app/build.gradle | 46 +++ .../app/src/main/AndroidManifest.xml | 72 ++++ .../iot/android/sense/RegisterActivity.java | 183 +++++++++ .../sense/constants/SenseConstants.java | 33 ++ .../data/publisher/DataPublisherReceiver.java | 36 ++ .../data/publisher/DataPublisherService.java | 171 ++++++++ .../android/sense/data/publisher/Event.java | 219 ++++++++++ .../mqtt/AndroidSenseMQTTHandler.java | 241 +++++++++++ .../mqtt/transport/MQTTTransportHandler.java | 385 ++++++++++++++++++ .../mqtt/transport/TransportHandler.java | 108 +++++ .../transport/TransportHandlerException.java | 56 +++ .../sense/event/SenseScheduleReceiver.java | 42 ++ .../iot/android/sense/event/SenseService.java | 63 +++ .../sense/event/streams/DataReader.java | 21 + .../event/streams/Location/LocationData.java | 56 +++ .../streams/Location/LocationDataReader.java | 169 ++++++++ .../event/streams/SenseDataCollector.java | 44 ++ .../event/streams/Sensor/SensorData.java | 102 +++++ .../streams/Sensor/SensorDataReader.java | 115 ++++++ .../event/streams/battery/BatteryData.java | 124 ++++++ .../streams/battery/BatteryDataReceiver.java | 38 ++ .../realtimeviewer/ActivitySelectSensor.java | 280 +++++++++++++ .../realtimeviewer/datastore/TempStore.java | 37 ++ .../event/RealTimeSensorChangeReceiver.java | 41 ++ .../event/realtimesensor/RealTimeSensor.java | 88 ++++ .../realtimesensor/RealTimeSensorReader.java | 62 +++ .../AvailableSensorsInDevice.java | 65 +++ .../sensorlisting/SupportedSensors.java | 120 ++++++ .../view/adaptor/SensorViewAdaptor.java | 97 +++++ .../sensor/selector/SelectSensorDialog.java | 147 +++++++ .../sense/speech/detector/IVoiceControl.java | 32 ++ .../detector/WordRecognitionActivity.java | 132 ++++++ .../detector/util/ListeningActivity.java | 125 ++++++ .../speech/detector/util/ProcessWords.java | 193 +++++++++ .../detector/util/StringSimilarity.java | 67 +++ .../util/VoiceRecognitionListener.java | 80 ++++ .../sense/speech/detector/util/WordData.java | 47 +++ .../iot/android/sense/util/LocalRegistry.java | 225 ++++++++++ .../iot/android/sense/util/SenseClient.java | 85 ++++ .../sense/util/SenseClientAsyncExecutor.java | 163 ++++++++ .../android/sense/util/SenseDataHolder.java | 78 ++++ .../iot/android/sense/util/SenseUtils.java | 46 +++ .../iot/android/sense/util/SenseWakeLock.java | 47 +++ .../sense/util/dto/AccessTokenInfo.java | 40 ++ .../util/dto/AndroidSenseManagerService.java | 13 + .../ApiApplicationRegistrationService.java | 25 ++ .../util/dto/ApiRegistrationProfile.java | 64 +++ .../dto/DynamicClientRegistrationService.java | 40 ++ .../sense/util/dto/OAuthApplicationInfo.java | 44 ++ .../util/dto/OAuthRequestInterceptor.java | 26 ++ .../sense/util/dto/RegistrationProfile.java | 67 +++ .../sense/util/dto/TokenIssuerService.java | 16 + .../app/src/main/res/drawable/mic.png | Bin 0 -> 26775 bytes .../src/main/res/drawable/pushtoserver.png | Bin 0 -> 57808 bytes .../app/src/main/res/drawable/sensor.png | Bin 0 -> 13854 bytes .../src/main/res/drawable/side_nav_bar.xml | 9 + .../activity_activity_select_sensor.xml | 18 + .../src/main/res/layout/activity_register.xml | 79 ++++ .../res/layout/activity_sense_settings.xml | 16 + .../res/layout/activity_speech_sense_main.xml | 86 ++++ .../layout/app_bar_activity_select_sensor.xml | 53 +++ .../layout/content_activity_select_sensor.xml | 28 ++ .../main/res/layout/display_sensor_values.xml | 56 +++ .../layout/fragment_select_sensor_dialog.xml | 10 + .../nav_header_activity_select_sensor.xml | 20 + ...activity_activity_select_sensor_drawer.xml | 22 + .../main/res/menu/activity_select_sensor.xml | 6 + .../src/main/res/menu/menu_sense_settings.xml | 7 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../app/src/main/res/mipmap-hdpi/wso2logo.png | Bin 0 -> 30848 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../src/main/res/raw/client_truststore.bks | Bin 0 -> 36583 bytes .../app/src/main/res/raw/wso2carbon.cer | Bin 0 -> 569 bytes .../app/src/main/res/values-v21/styles.xml | 8 + .../app/src/main/res/values-w820dp/dimens.xml | 6 + .../app/src/main/res/values/device.xml | 4 + .../app/src/main/res/values/dimens.xml | 9 + .../app/src/main/res/values/strings.xml | 14 + .../res/values/strings_activity_register.xml | 13 + .../app/src/main/res/values/styles.xml | 14 + .../build.gradle | 21 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../gradlew | 164 ++++++++ .../gradlew.bat | 90 ++++ .../pom.xml | 54 +++ .../settings.gradle | 1 + .../impl/AndroidSenseControllerService.java | 163 +------- .../AndroidSenseControllerServiceImpl.java | 323 +++------------ .../impl/AndroidSenseManagerService.java | 6 +- .../impl/AndroidSenseManagerServiceImpl.java | 17 +- .../transport/AndroidSenseMQTTConnector.java | 199 +-------- .../src/main/webapp/META-INF/permissions.xml | 119 +----- .../src/main/webapp/WEB-INF/web.xml | 14 +- .../src/main/java/SubscriptionEndpoint.java | 3 +- .../java/SuperTenantSubscriptionEndpoint.java | 2 +- .../main/java/TenantSubscriptionEndpoint.java | 2 +- .../SuperTenantEventRetrievalEndpoint.java | 85 ---- .../servlet/TenantEventRetrievalEndpoint.java | 89 ---- .../servlet => util}/ServiceHolder.java | 5 +- .../src/main/webapp/WEB-INF/web.xml | 17 - components/iot-plugins/das-extensions/pom.xml | 2 +- .../util/DigitalDisplayMQTTConnector.java | 3 + .../transport/mqtt/MQTTTransportHandler.java | 9 +- .../transport/RaspberryPiMQTTConnector.java | 3 + .../VirtualFireAlarmMQTTConnector.java | 3 + 108 files changed, 5859 insertions(+), 942 deletions(-) create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/mic.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/pushtoserver.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/sensor.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/side_nav_bar.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_sense_settings.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_speech_sense_main.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/app_bar_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/content_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/display_sensor_values.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/fragment_select_sensor_dialog.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/nav_header_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/menu/activity_activity_select_sensor_drawer.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/menu/activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/menu/menu_sense_settings.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-hdpi/wso2logo.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/raw/client_truststore.bks create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/raw/wso2carbon.cer create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values-v21/styles.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values-w820dp/dimens.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/device.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/dimens.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/strings.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/strings_activity_register.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/styles.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/build.gradle create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/gradle/wrapper/gradle-wrapper.properties create mode 100755 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/gradlew create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/gradlew.bat create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/pom.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/settings.gradle delete mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/SuperTenantEventRetrievalEndpoint.java delete mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/TenantEventRetrievalEndpoint.java rename components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/{org/wso2/carbon/servlet => util}/ServiceHolder.java (82%) diff --git a/.gitignore b/.gitignore index cd8838ac6d..ac1222119b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ target # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# Android Studio +.gradle +build/ + +# Local configuration file (sdk path, etc) +local.properties diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle new file mode 100644 index 0000000000..1fd0aa8186 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion '22.0.1' + defaultConfig { + applicationId "agent.sense.android.iot.carbon.wso2.org.wso2_senseagent" + minSdkVersion 19 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + applicationVariants.all { variant -> + variant.outputs.each { output -> + def newName = output.outputFile.name + newName = newName.replace("app-", "androidsense") + newName = newName.replace("release", "") + //noinspection GroovyAssignabilityCheck + output.outputFile = new File(output.outputFile.parent, newName) + } + } + } + } + packagingOptions { + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + } + productFlavors { + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:22.2.1' + compile 'com.android.support:design:22.2.1' + compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.0.2' + compile 'com.github.rholder:snowball-stemmer:1.3.0.581.1' + compile 'commons-codec:commons-codec:1.4' + compile 'com.netflix.feign:feign-jaxrs:8.16.0' + compile 'com.netflix.feign:feign-jackson:8.16.0' + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5a799fbdad --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java new file mode 100644 index 0000000000..fceeeeedec --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; + +import org.wso2.carbon.iot.android.sense.data.publisher.DataPublisherReceiver; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.AndroidSenseMQTTHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; +import org.wso2.carbon.iot.android.sense.event.SenseScheduleReceiver; +import org.wso2.carbon.iot.android.sense.realtimeviewer.ActivitySelectSensor; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.AvailableSensorsInDevice; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; +import org.wso2.carbon.iot.android.sense.util.SenseClient; +import org.wso2.carbon.iot.android.sense.util.SenseUtils; +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + + +/** + * A login screen that offers to register the device. + */ +public class RegisterActivity extends Activity { + + private EditText mUsernameView; + private EditText mPasswordView; + private EditText mHostView; + private EditText mMqttPortView; + private View mProgressView; + private View mLoginFormView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getSharedPreferences(SupportedSensors.SELECTED_SENSORS, 0).edit().clear().apply(); + + if (LocalRegistry.isExist(getApplicationContext())) { + Intent intent = new Intent(getApplicationContext(), ActivitySelectSensor.class); + startActivity(intent); + } + setContentView(R.layout.activity_register); + mUsernameView = (EditText) findViewById(R.id.username); + mPasswordView = (EditText) findViewById(R.id.password); + mHostView = (EditText) findViewById(R.id.hostname); + mMqttPortView = (EditText) findViewById(R.id.mqttPort); + AvailableSensorsInDevice availableSensorsInDevice = new AvailableSensorsInDevice(getApplicationContext()); + availableSensorsInDevice.setContent(); + + Button deviceRegisterButton = (Button) findViewById(R.id.device_register_button); + + + deviceRegisterButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + attemptLogin(); + } + }); + + mLoginFormView = findViewById(R.id.login_form); + mProgressView = findViewById(R.id.login_progress); + } + + public void attemptLogin() { + showProgress(true); + // Reset errors. + mUsernameView.setError(null); + mPasswordView.setError(null); + + // Store values at the time of the login attempt. + String username = mUsernameView.getText().toString(); + String password = mPasswordView.getText().toString(); + String hostname = mHostView.getText().toString(); + String mqttPort = mMqttPortView.getText().toString(); + boolean cancel = false; + View focusView = null; + + // Check for a valid password, if the user entered one. + if (!TextUtils.isEmpty(password)) { + // mPasswordView.setError(getString(R.string.error_invalid_password)); + focusView = mPasswordView; + //cancel = true; + } + // Check for a valid username . + if (TextUtils.isEmpty(username)) { + mUsernameView.setError(getString(R.string.error_field_required)); + focusView = mUsernameView; + cancel = true; + } + if (TextUtils.isEmpty(username)) { + mHostView.setError(getString(R.string.error_field_required)); + focusView = mHostView; + cancel = true; + } + + if (cancel) { + focusView.requestFocus(); + } else { + SenseClient client = new SenseClient(getApplicationContext()); + LocalRegistry.addServerURL(getBaseContext(), hostname); + String deviceId = SenseUtils.generateDeviceId(getBaseContext(), getContentResolver()); + boolean registerStatus = client.register(username, password, deviceId); + if (registerStatus) { + LocalRegistry.addUsername(getApplicationContext(), username); + LocalRegistry.addDeviceId(getApplicationContext(), deviceId); + LocalRegistry.addMqttPort(getApplicationContext(), Integer.parseInt(mqttPort)); + MQTTTransportHandler mqttTransportHandler = AndroidSenseMQTTHandler.getInstance(this); + if (!mqttTransportHandler.isConnected()) { + mqttTransportHandler.connect(); + } + SenseScheduleReceiver senseScheduleReceiver = new SenseScheduleReceiver(); + senseScheduleReceiver.clearAbortBroadcast(); + senseScheduleReceiver.onReceive(this, null); + + DataPublisherReceiver dataUploaderReceiver = new DataPublisherReceiver(); + dataUploaderReceiver.clearAbortBroadcast(); + dataUploaderReceiver.onReceive(this, null); + + Intent intent = new Intent(getApplicationContext(), ActivitySelectSensor.class); + startActivity(intent); + } + showProgress(false); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + public void showProgress(final boolean show) { + // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow + // for very easy animations. If available, use these APIs to fade-in + // the progress spinner. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); + + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + mLoginFormView.animate().setDuration(shortAnimTime).alpha( + show ? 0 : 1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + }); + + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mProgressView.animate().setDuration(shortAnimTime).alpha( + show ? 1 : 0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + } + }); + } else { + // The ViewPropertyAnimator APIs are not available, so simply show + // and hide the relevant UI components. + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + } + +} + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java new file mode 100644 index 0000000000..70d847802a --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.constants; + +public class SenseConstants { + public final static String DEVICE_TYPE = "android_sense"; + public final static String REGISTER_CONTEXT = "/android_sense_mgt"; + public final static String DCR_CONTEXT = "/dynamic-client-web"; + public final static String TOKEN_ISSUER_CONTEXT = "/oauth2"; + public final static String API_APPLICATION_REGISTRATION_CONTEXT = "/api-application-registration"; + + public static final int MQTT_BROKER_PORT = 1883; + public static final String EVENT_LISTENER_STARTED = "xxStartedxx"; + public static final String EVENT_LISTENER_FINISHED = "xxFinishedxx"; + public static final String EVENT_LISTENER_ONGOING = "xxOngoingxx"; + + public final class Request { + public final static String REQUEST_SUCCESSFUL = "200"; + public final static String REQUEST_CONFLICT = "409"; + public final static int MAX_ATTEMPTS = 2; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java new file mode 100644 index 0000000000..b09d15a889 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.data.publisher; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * This creates and AlarmManagerService that triggers the data uploader service with a 30 seconds interval. + */ +public class DataPublisherReceiver extends BroadcastReceiver { + private static int ALARM_INTERVAL = 30000; + + @Override + public void onReceive(Context context, Intent intent) { + AlarmManager service = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(context, DataPublisherService.class); + PendingIntent pending = PendingIntent.getService(context, 0, i, 0); + service.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_INTERVAL, pending); + } + +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java new file mode 100644 index 0000000000..41853f8cda --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.data.publisher; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.hardware.Sensor; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.AndroidSenseMQTTHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.TransportHandlerException; +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationData; +import org.wso2.carbon.iot.android.sense.event.streams.Sensor.SensorData; +import org.wso2.carbon.iot.android.sense.event.streams.battery.BatteryData; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ProcessWords; +import org.wso2.carbon.iot.android.sense.speech.detector.util.WordData; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; +//import org.wso2.carbon.iot.android.sense.util.SenseClient; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is an android service which publishes the data to the server. + */ +public class DataPublisherService extends Service { + private static final String TAG = "Data Publisher"; + private static String KEY_TAG = "key"; + private static String TIME_TAG = "time"; + private static String VALUE_TAG = "value"; + public static Context context; + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + context = this; + Log.d(TAG, "service started"); + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + List events = new ArrayList<>(); + //retreive sensor data. + List sensorDataMap = SenseDataHolder.getSensorDataHolder(); + for (SensorData sensorData : sensorDataMap) { + Event event = new Event(); + event.setTimestamp(sensorData.getTimestamp()); + + switch (sensorData.getSensorType()) { + case Sensor.TYPE_ACCELEROMETER: + event.setAccelerometer(sensorData.getSensorValues()); + break; + case Sensor.TYPE_MAGNETIC_FIELD: + event.setMagnetic(sensorData.getSensorValues()); + break; + case Sensor.TYPE_GYROSCOPE: + event.setGyroscope(sensorData.getSensorValues()); + break; + case Sensor.TYPE_LIGHT: + event.setLight(sensorData.getSensorValues()[0]); + break; + case Sensor.TYPE_PRESSURE: + event.setPressure(sensorData.getSensorValues()[0]); + break; + case Sensor.TYPE_PROXIMITY: + event.setProximity(sensorData.getSensorValues()[0]); + break; + case Sensor.TYPE_GRAVITY: + event.setGravity(sensorData.getSensorValues()); + break; + case Sensor.TYPE_ROTATION_VECTOR: + event.setGravity(sensorData.getSensorValues()); + break; + } + events.add(event); + } + SenseDataHolder.resetSensorDataHolder(); + + //retreive batter data. + List batteryDataMap = SenseDataHolder.getBatteryDataHolder(); + for (BatteryData batteryData : batteryDataMap) { + Event event = new Event(); + event.setTimestamp(batteryData.getTimestamp()); + event.setBattery(batteryData.getLevel()); + events.add(event); + } + SenseDataHolder.resetBatteryDataHolder(); + //retreive location data. + List locationDataMap = SenseDataHolder.getLocationDataHolder(); + for (LocationData locationData : locationDataMap) { + Event event = new Event(); + event.setTimestamp(locationData.getTimeStamp()); + event.setGps(new double[]{locationData.getLatitude(), locationData.getLongitude()}); + events.add(event); + } + SenseDataHolder.resetLocationDataHolder(); + + //retreive words + ProcessWords.cleanAndPushToWordMap(); + List wordDatMap = SenseDataHolder.getWordDataHolder(); + for (WordData wordData : wordDatMap) { + if (wordData.getOccurences() == 0) { + continue; + } + for (int i = 0; i < wordData.getOccurences(); i++) { + Event event = new Event(); + event.setTimestamp(wordData.getTimestamp()); + event.setWord(wordData.getWord()); + String word = wordData.getWord(); + String status = word; + if ((!word.equals(SenseConstants.EVENT_LISTENER_STARTED)) && (!word.equals(SenseConstants + .EVENT_LISTENER_FINISHED))) { + status = SenseConstants.EVENT_LISTENER_ONGOING; + } + event.setWordStatus(status); + events.add(event); + } + } + SenseDataHolder.resetWordDataHolder(); + //publish the data + if (events.size() > 0) { + String user = LocalRegistry.getUsername(context); + String deviceId = LocalRegistry.getDeviceId(context); + JSONArray jsonArray = new JSONArray(); + for (Event event : events) { + event.setOwner(user); + event.setDeviceId(deviceId); + jsonArray.put(event.getEvent()); + } + MQTTTransportHandler mqttTransportHandler = AndroidSenseMQTTHandler.getInstance(context); + if (!mqttTransportHandler.isConnected()) { + mqttTransportHandler.connect(); + } + mqttTransportHandler.publishDeviceData(user, deviceId, jsonArray.toString()); + } + } catch (JSONException e) { + Log.e(TAG, "Json Data Parsing Exception", e); + } catch (TransportHandlerException e) { + Log.e(TAG, "Data Publish Failed", e); + } + } + }; + Thread dataUploaderThread = new Thread(runnable); + dataUploaderThread.start(); + return Service.START_NOT_STICKY; + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java new file mode 100644 index 0000000000..e7ceea0631 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java @@ -0,0 +1,219 @@ +package org.wso2.carbon.iot.android.sense.data.publisher; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Event { + + private String owner; + private String deviceId; + private String type; + private float battery; + private double gps[]; //lat,long + private float accelerometer[]; //x,y,z + private float magnetic[]; //x,y,z + private float gyroscope[]; //x,y,z + private float light; + private float pressure; + private float proximity; + private float gravity[]; + private float rotation[]; + private String wordSessionId; + private String word; + private String wordStatus; + private long timestamp; + + private float getBattery() { + return battery; + } + + public void setBattery(float battery) { + this.type = "battery"; + this.battery = battery; + } + + private double[] getGps() { + return gps != null ? gps : new double[]{0, 0}; + } + + public void setGps(double[] gps) { + this.type = "gps"; + this.gps = gps; + } + + private float[] getAccelerometer() { + return accelerometer != null ? accelerometer : new float[]{0, 0, 0}; + } + + public void setAccelerometer(float[] accelerometer) { + this.type = "accelerometer"; + this.accelerometer = accelerometer; + } + + private float[] getMagnetic() { + return magnetic != null ? magnetic : new float[]{0, 0, 0}; + } + + public void setMagnetic(float[] magnetic) { + this.type = "magnetic"; + this.magnetic = magnetic; + } + + private float[] getGyroscope() { + return gyroscope != null ? gyroscope : new float[]{0, 0, 0}; + } + + public void setGyroscope(float[] gyroscope) { + this.type = "gyroscope"; + this.gyroscope = gyroscope; + } + + public float getLight() { + return light; + } + + public void setLight(float light) { + this.type = "light"; + this.light = light; + } + + public float getPressure() { + return pressure; + } + + public void setPressure(float pressure) { + this.type = "pressure"; + this.pressure = pressure; + } + + public float getProximity() { + return proximity; + } + + public void setProximity(float proximity) { + this.type = "proximity"; + this.proximity = proximity; + } + + private float[] getGravity() { + return gravity != null ? gravity : new float[]{0, 0, 0}; + } + + public void setGravity(float gravity[]) { + this.type = "gravity"; + this.gravity = gravity; + } + + private float[] getRotation() { + return rotation != null ? rotation : new float[]{0, 0, 0}; + } + + public void setRotation(float rotation[]) { + this.type = "rotation"; + this.rotation = rotation; + } + + private String getWordSessionId() { + return wordSessionId != null ? wordSessionId : ""; + } + + public void setWordSessionId(String wordSessionId) { + this.wordSessionId = wordSessionId; + } + + private String getWord() { + return word != null ? word : ""; + } + + public void setWord(String word) { + this.type = "word"; + this.word = word; + } + + private long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + private String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + private String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getWordStatus() { + return wordStatus != null ? wordStatus : ""; + } + + public void setWordStatus(String wordStatus) { + this.wordStatus = wordStatus; + } + + public JSONObject getEvent() throws JSONException { + JSONObject jsonEvent = new JSONObject(); + JSONObject jsonMetaData = new JSONObject(); + jsonMetaData.put("owner", getOwner()); + jsonMetaData.put("deviceId", getDeviceId()); + jsonMetaData.put("type", type); + jsonMetaData.put("timestamp", getTimestamp()); + jsonEvent.put("metaData", jsonMetaData); + + JSONObject jsonPayloadData = new JSONObject(); + jsonPayloadData.put("battery", getBattery()); + //gps + double gpsEvents[] = getGps(); + jsonPayloadData.put("gps_lat", gpsEvents[0]); + jsonPayloadData.put("gps_long", gpsEvents[1]); + //acceleromter + float events[] = getAccelerometer(); + jsonPayloadData.put("accelerometer_x", events[0]); + jsonPayloadData.put("accelerometer_y", events[1]); + jsonPayloadData.put("accelerometer_z", events[2]); + //magnetic + events = getMagnetic(); + jsonPayloadData.put("magnetic_x", events[0]); + jsonPayloadData.put("magnetic_y", events[1]); + jsonPayloadData.put("magnetic_z", events[2]); + //gyroscope + events = getGyroscope(); + jsonPayloadData.put("gyroscope_x", events[0]); + jsonPayloadData.put("gyroscope_y", events[1]); + jsonPayloadData.put("gyroscope_z", events[2]); + + jsonPayloadData.put("light", getLight()); + jsonPayloadData.put("pressure", getPressure()); + jsonPayloadData.put("proximity", getProximity()); + //gravity + events = getGravity(); + jsonPayloadData.put("gravity_x", events[0]); + jsonPayloadData.put("gravity_y", events[1]); + jsonPayloadData.put("gravity_z", events[2]); + //rotation + events = getRotation(); + jsonPayloadData.put("rotation_x", events[0]); + jsonPayloadData.put("rotation_y", events[1]); + jsonPayloadData.put("rotation_z", events[2]); + //word + jsonPayloadData.put("word", getWord()); + jsonPayloadData.put("word_sessionId", getWordSessionId()); + jsonPayloadData.put("word_status", getWordStatus()); + + jsonEvent.put("payloadData", jsonPayloadData); + + return jsonEvent; + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java new file mode 100644 index 0000000000..5ae9b37391 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt; + +import android.content.Context; +import android.util.Log; + +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.TransportHandlerException; +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ProcessWords; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * This is an example for the use of the MQTT capabilities provided by the IoT-Server. This example depicts the use + * of MQTT Transport for the Android Sense device-type. This class extends the abstract class + * "MQTTTransportHandler". "MQTTTransportHandler" consists of the MQTT client specific functionality and implements + * the "TransportHandler" interface. The actual functionality related to the "TransportHandler" interface is + * implemented here, in this concrete class. Whilst the abstract class "MQTTTransportHandler" is intended to provide + * the common MQTT functionality, this class (which is its extension) provides the implementation specific to the + * MQTT communication of the Device-Type (Android Sense) in concern. + *

+ * Hence, the methods of this class are implementation of the "TransportHandler" interface which handles the device + * specific logic to connect-to, publish-to, process-incoming-messages-from and disconnect-from the MQTT broker + * listed in the configurations. + */ +public class AndroidSenseMQTTHandler extends MQTTTransportHandler { + private static final String TAG = "AndroidSenseMQTTHandler"; + private static volatile AndroidSenseMQTTHandler mInstance; + + + /** + * return a sigleton Instance + * @param context is the android context object. + * @return AndroidSenseMQTTHandler. + */ + public static AndroidSenseMQTTHandler getInstance(Context context) { + if (mInstance == null) { + Class clazz = AndroidSenseMQTTHandler.class; + synchronized (clazz) { + if (mInstance == null) { + mInstance = new AndroidSenseMQTTHandler(context); + } + } + } + return mInstance; + } + + /** + * Default constructor for the AndroidSenseMQTTHandler. + */ + private AndroidSenseMQTTHandler(Context context) { + super(context); + } + + /** + * {@inheritDoc} + * AndroidSense device-type specific implementation to connect to the MQTT broker and subscribe to a topic. + * This method is called to initiate a MQTT communication. + */ + @Override + public void connect() { + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + connectToQueue(); + } catch (TransportHandlerException e) { + Log.e(TAG, "Connection to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Log.e(TAG, "MQTT-Connector: Thread Sleep Interrupt Exception.", ex); + } + } + + try { + subscribeToQueue(); + } catch (TransportHandlerException e) { + Log.w(TAG, "Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + } + } + } + }; + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + /** + * {@inheritDoc} + * AndroidSense device-type specific implementation to process incoming messages. This is the specific + * method signature of the overloaded "processIncomingMessage" method that gets called from the messageArrived() + * callback of the "MQTTTransportHandler". + */ + @Override + public void processIncomingMessage(MqttMessage mqttMessage, String... messageParams) { + if (messageParams.length != 0) { + // owner and the deviceId are extracted from the MQTT topic to which the message was received. + // = [ServerName/Owner/DeviceType/DeviceId/#] + String topic = messageParams[0]; + String[] topicParams = topic.split("/"); + String owner = topicParams[1]; + String deviceId = topicParams[3]; + + Log.d(TAG, "Received MQTT message for: [OWNER-" + owner + "] & [DEVICE.ID-" + deviceId + "]"); + + String msg; + msg = mqttMessage.toString(); + Log.d(TAG, "MQTT: Received Message [" + msg + "] topic: [" + topic + "]"); + if (topic.contains("threshold")) { + try { + ProcessWords.setThreshold(Integer.parseInt(msg)); + } catch (NumberFormatException e) { + Log.e(TAG, "Invalid threshold value " + msg); + } + } else if (topic.contains("words")) { + String words[] = msg.split(" "); + ProcessWords.addWords(Arrays.asList(words)); + } else if (topic.contains("remove")) { + String words[] = msg.split(" "); + for (String word: words) { + ProcessWords.removeWord(word); + } + } + } else { + String errorMsg = + "MQTT message [" + mqttMessage.toString() + "] was received without the topic information."; + Log.w(TAG, errorMsg); + } + } + + /** + * {@inheritDoc} + * AndroidSense device-type specific implementation to publish data to the device. This method calls the + * {@link #publishToQueue(String, MqttMessage)} method of the "MQTTTransportHandler" class. + */ + @Override + public void publishDeviceData(String... publishData) throws TransportHandlerException { + if (publishData.length != 3) { + String errorMsg = "Incorrect number of arguments received to SEND-MQTT Message. " + + "Need to be [owner, deviceId, content]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg); + } + + String deviceOwner = publishData[0]; + String deviceId = publishData[1]; + String resource = publishData[2]; + + MqttMessage pushMessage = new MqttMessage(); + String publishTopic = "wso2/" + SenseConstants.DEVICE_TYPE + "/" + deviceId + "/data"; + String actualMessage = resource; + pushMessage.setPayload(actualMessage.getBytes(StandardCharsets.UTF_8)); + pushMessage.setQos(DEFAULT_MQTT_QUALITY_OF_SERVICE); + pushMessage.setRetained(false); + publishToQueue(publishTopic, pushMessage); + } + + + /** + * {@inheritDoc} + * Android Sense device-type specific implementation to disconnect from the MQTT broker. + */ + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + closeConnection(); + } catch (MqttException e) { + Log.w(TAG, "Unable to 'STOP' MQTT connection at broker at: " + mqttBrokerEndPoint + + " for device-type - " + SenseConstants.DEVICE_TYPE, e); + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + Log.e(TAG, "MQTT-Terminator: Thread Sleep Interrupt Exception at device-type - " + + SenseConstants.DEVICE_TYPE, e1); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + + /** + * {@inheritDoc} + */ + @Override + public void publishDeviceData() { + // nothing to do + } + + @Override + public void publishDeviceData(MqttMessage publishData) throws TransportHandlerException { + } + + /** + * {@inheritDoc} + */ + @Override + public void processIncomingMessage() { + // nothing to do + } + + @Override + public void processIncomingMessage(MqttMessage message) throws TransportHandlerException { + + } + +} + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java new file mode 100644 index 0000000000..689138d729 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport; +import android.content.Context; +import android.util.Log; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; + +import java.nio.charset.StandardCharsets; + +/** + * This is an abstract class that implements the "TransportHandler" interface. The interface is an abstraction for + * the core functionality with regards to device-server communication regardless of the Transport protocol. This + * specific class contains the MQTT-Transport specific implementations. The class implements utility methods for the + * case of a MQTT communication. However, this "abstract class", even-though it implements the "TransportHandler" + * interface, does not contain the logic relevant to the interface methods. The specific functionality of the + * interface methods are intended to be implemented by the concrete class that extends this abstract class and + * utilizes the MQTT specific functionality (ideally a device API writer who would like to communicate to the device + * via MQTT Protocol). + *

+ * This class contains the Device-Management specific implementation for all the MQTT functionality. This includes + * connecting to a MQTT Broker & subscribing to the appropriate MQTT-topic, action plan upon losing connection or + * successfully delivering a message to the broker and upon receiving a MQTT message. Makes use of the 'Paho-MQTT' + * library provided by Eclipse Org. + */ +public abstract class MQTTTransportHandler implements MqttCallback, TransportHandler { + private static final String TAG = "MQTTTransportHandler"; + + private MqttClient client; + private String clientId; + private MqttConnectOptions options; // options to be set to the client-connection. + // topic to which a will-message is automatically published by the broker upon the device losing its connection. + private String clientWillTopic; + + protected String mqttBrokerEndPoint; + protected int timeoutInterval; // interval to use for reconnection attempts etc. + protected String subscribeTopic; + + // Quality of Service Levels for MQTT Subscription and Publishing. + public static final int QoS_0 = 0; // At-Most Once + @SuppressWarnings("unused") + public static final int QoS_1 = 1; // At-Least Once + public static final int QoS_2 = 2; // Exactly Once + + public static final int DEFAULT_MQTT_QUALITY_OF_SERVICE = QoS_0; + // Prefix to the Will-Topic to which a message is published if client loses its connection. + private static final String DISCONNECTION_WILL_TOPIC_PREFIX = "Disconnection/"; + // Will-Message of the client to be published if connection is lost. + private static final String DISCONNECTION_WILL_MSG = "Lost-Connection"; + /** + * Constructor for the MQTTTransportHandler which takes in the owner, type of the device and the MQTT Broker URL + * and the topic to subscribe. + * @param context activity context. + */ + protected MQTTTransportHandler(Context context) { + String username = LocalRegistry.getUsername(context); + String deviceId = LocalRegistry.getDeviceId(context); + this.clientId = deviceId + ":" + SenseConstants.DEVICE_TYPE; + this.subscribeTopic = "wso2/" + SenseConstants.DEVICE_TYPE + "/" + deviceId + "/command/#"; + this.clientWillTopic = DISCONNECTION_WILL_TOPIC_PREFIX + SenseConstants.DEVICE_TYPE; + this.mqttBrokerEndPoint = "tcp://" + LocalRegistry.getServerHost(context) + ":" + LocalRegistry.getMqttPort(context); + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + setUsernameAndPassword(LocalRegistry.getAccessToken(context), ""); + this.initMQTTClient(); + } + + /** + * Constructor for the MQTTTransportHandler which takes in the owner, type of the device and the MQTT Broker URL + * and the topic to subscribe. Additionally this constructor takes in the reconnection-time interval between + * successive attempts to connect to the broker. + * + * @param deviceOwner the owner of the device. + * @param deviceType the CDMF Device-Type of the device. + * @param mqttBrokerEndPoint the IP/URL of the MQTT broker endpoint. + * @param subscribeTopic the MQTT topic to which the client is to be subscribed + * @param intervalInMillis the time interval in MILLI-SECONDS between attempts to connect to the broker. + */ + protected MQTTTransportHandler(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, String subscribeTopic, int intervalInMillis) { + this.clientId = deviceOwner + ":" + deviceType; + this.subscribeTopic = subscribeTopic; + this.clientWillTopic = DISCONNECTION_WILL_TOPIC_PREFIX + deviceType; + this.mqttBrokerEndPoint = mqttBrokerEndPoint; + this.timeoutInterval = intervalInMillis; + this.initMQTTClient(); + } + + + /** + * Initializes the MQTT-Client. Creates a client using the given MQTT-broker endpoint and the clientId (which is + * constructed by a concatenation of [deviceOwner]:[deviceType]). Also sets the client's options parameter with + * the clientWillTopic (in-case of connection failure) and other info. Also sets the callback to this current class. + */ + private void initMQTTClient() { + try { + client = new MqttClient(this.mqttBrokerEndPoint, clientId, null); + Log.i(TAG, "MQTT client was created with ClientID : " + clientId); + } catch (MqttException ex) { + String errorMsg = "Initializing the MQTT Client failed."; + Log.e(TAG, errorMsg, ex); + } + + options = new MqttConnectOptions(); + options.setKeepAliveInterval(120); // set the keep alive interval to 120 seconds by default. + options.setCleanSession(true); // sets clean session to true by default. + setDisconnectionWillForClient(QoS_2, true); // sets default will-topic & msg with QoS 2 and retained true. + client.setCallback(this); // callback for MQTT events are set to `this` object. + } + + /** + * @param qos the Quality of Service at which the last-will-message is to be published. + * @param isRetained indicate whether to retain the last-will-message. + * @see MQTTTransportHandler#setDisconnectionWillForClient(String, String, int, boolean). Uses the default values + * for Will-Topic and Will-Message. + */ + protected void setDisconnectionWillForClient(int qos, boolean isRetained) { + this.setDisconnectionWillForClient(clientWillTopic, DISCONNECTION_WILL_MSG, qos, isRetained); + } + + /** + * Sets the [Will] option in the default options-set of the MQTT Client. A will-topic, will-message is parsed + * along with the QoS and the retained flag. When the client loses its connection to the broker, the broker + * publishes the will-message to the will-topic, to itself. + * + * @param willTopic the topic to which the last will message is to be published when client exists ungracefully. + * @param willMsg the message to be published upon client's ungraceful exit from the broker. + * @param qos the Quality of Service at which the last-will-message is to be published. + * @param isRetained indicate whether to retain the last-will-message. + */ + protected void setDisconnectionWillForClient(String willTopic, String willMsg, int qos, boolean isRetained) { + this.options.setWill(willTopic, willMsg.getBytes(StandardCharsets.UTF_8), qos, isRetained); + } + + /** + * Sets the [Clean-Session] option in the default options-set of the MQTT Client. It is set to `true` by default. + * + * @param setCleanSession `true` indicates that the session details can be cleared/cleaned upon disconnection, + * `false` indicates that the session details are to be persisted if the client disconnects. + */ + @SuppressWarnings("unused") + protected void setClientCleanSession(boolean setCleanSession) { + this.options.setCleanSession(setCleanSession); + } + + /** + * Sets the [Username] & [Password] options in the default options-set of the MQTT Client. By default these + * values are not set. + * + * @param username the username to be used by the client to connect to the broker. + * @param password the password to be used by the client to connect to the broker. + */ + @SuppressWarnings("unused") + protected void setUsernameAndPassword(String username, String password) { + this.options.setUserName(username); + this.options.setPassword(password.toCharArray()); + } + + /** + * Connects to the MQTT-Broker at the endpoint specified in the constructor to this class using default the + * MQTT-options. + * + * @throws TransportHandlerException in the event of 'Connecting to' the MQTT broker fails. + */ + protected void connectToQueue() throws TransportHandlerException { + this.connectToQueue(options); + } + + /** + * Connects to the MQTT-Broker at the endpoint specified in the constructor to this class using the MQTT-Options + * passed. + * + * @param options options to be used by the client for this connection. (username, password, clean-session, etc) + * @throws TransportHandlerException in the event of 'Connecting to' the MQTT broker fails. + */ + protected void connectToQueue(MqttConnectOptions options) throws TransportHandlerException { + try { + client.connect(options); + Log.d(TAG, "MQTT Client connected to queue at: " + this.mqttBrokerEndPoint); + } catch (MqttException ex) { + String errorMsg = "MQTT Exception occured whilst connecting to queue at [" + this.mqttBrokerEndPoint + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * @throws TransportHandlerException in the event of 'Subscribing to' the MQTT broker fails. + * @see MQTTTransportHandler#subscribeToQueue(int). Uses default QoS of 1. + */ + protected void subscribeToQueue() throws TransportHandlerException { + this.subscribeToQueue(QoS_0); + } + + /** + * Subscribes to the MQTT-Topic specified in the constructor to this class. + * + * @throws TransportHandlerException in the event of 'Subscribing to' the MQTT broker fails. + */ + protected void subscribeToQueue(int qos) throws TransportHandlerException { + try { + client.subscribe(subscribeTopic, qos); + Log.d(TAG, "Client [" + clientId + "] subscribed to topic: " + subscribeTopic); + } catch (MqttException ex) { + String errorMsg = "MQTT Exception occurred whilst client [" + clientId + "] tried to subscribe to " + + "topic: [" + subscribeTopic + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * @param topic the topic to which the message is to be published. + * @param payLoad the message (payload) of the MQTT publish action. + * @see MQTTTransportHandler#publishToQueue(String, String, int, boolean) + */ + @SuppressWarnings("unused") + protected void publishToQueue(String topic, String payLoad) throws TransportHandlerException { + publishToQueue(topic, payLoad, DEFAULT_MQTT_QUALITY_OF_SERVICE, false); + } + + /** + * @param topic the topic to which the message is to be published. + * @param message the message (payload) of the MQTT publish action as a `MQTTMessage`. + * @throws TransportHandlerException if any error occurs whilst trying to publish to the MQTT Queue. + * @see MQTTTransportHandler#publishToQueue(String, String, int, boolean) + */ + protected void publishToQueue(String topic, MqttMessage message) throws TransportHandlerException { + try { + client.publish(topic, message); + Log.d(TAG, "Message: " + message.toString() + " to MQTT topic [" + topic + "] published successfully"); + } catch (MqttException ex) { + String errorMsg = "MQTT Client Error whilst client [" + clientId + "] tried to publish to queue at " + + "[" + mqttBrokerEndPoint + "] under topic [" + topic + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * This method is used to publish messages to the MQTT-Endpoint to which this client is connected to. It is via + * publishing to this broker that the messages are communicated to the device. This is an overloaded method with + * different parameter combinations. This method invokes the publish method provided by the MQTT-Client library. + * + * @param topic the topic to which the message is to be published. + * @param payLoad the message (payload) of the MQTT publish action. + * @param qos the Quality-of-Service of the current publish action. + * Could be 0(At-most once), 1(At-least once) or 2(Exactly once) + * @param retained indicate whether to retain the publish-message in the event of no subscribers. + * @throws TransportHandlerException if any error occurs whilst trying to publish to the MQTT Queue. + */ + protected void publishToQueue(String topic, String payLoad, int qos, boolean retained) + throws TransportHandlerException { + try { + client.publish(topic, payLoad.getBytes(StandardCharsets.UTF_8), qos, retained); + Log.d(TAG, "Message: " + payLoad + " to MQTT topic [" + topic + "] published successfully"); + + } catch (MqttException ex) { + String errorMsg = "MQTT Client Error whilst client [" + clientId + "] tried to publish to queue at " + + "[" + mqttBrokerEndPoint + "] under topic [" + topic + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * Checks whether the connection to the MQTT-Broker exists. + * + * @return `true` if the client is connected to the MQTT-Broker, else `false`. + */ + @Override + public boolean isConnected() { + return client.isConnected(); + } + + /** + * Callback method which is triggered once the MQTT client losers its connection to the broker. Spawns a new + * thread that executes necessary actions to try and reconnect to the endpoint. + * + * @param throwable a Throwable Object containing the details as to why the failure occurred. + */ + @Override + public void connectionLost(Throwable throwable) { + Log.w(TAG, "Connection for client: " + this.clientId + " to " + this.mqttBrokerEndPoint + " was lost." + + "\nThis was due to - " + throwable.getMessage()); + Thread reconnectThread = new Thread() { + public void run() { + connect(); + } + }; + reconnectThread.setDaemon(true); + reconnectThread.start(); + } + + /** + * Callback method which is triggered upon receiving a MQTT Message from the broker. Spawns a new thread that + * executes any actions to be taken with the received message. + * + * @param topic the MQTT-Topic to which the received message was published to and the client subscribed to. + * @param mqttMessage the actual MQTT-Message that was received from the broker. + */ + @Override + public void messageArrived(final String topic, final MqttMessage mqttMessage) { + Log.d(TAG, "Got an MQTT message '" + mqttMessage.toString() + "' for topic '" + topic + "'."); + + Thread messageProcessorThread = new Thread() { + public void run() { + try { + processIncomingMessage(mqttMessage, topic); + } catch (TransportHandlerException e) { + Log.e(TAG, "An error occurred when trying to process received MQTT message [" + mqttMessage + "] " + + "for topic [" + topic + "].", e); + } + } + }; + messageProcessorThread.setDaemon(true); + messageProcessorThread.start(); + } + + /** + * Callback method which gets triggered upon successful completion of a message delivery to the broker. + * + * @param iMqttDeliveryToken the MQTT-DeliveryToken which includes the details about the specific message delivery. + */ + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + String topic = iMqttDeliveryToken.getTopics()[0]; + String client = iMqttDeliveryToken.getClient().getClientId(); + + try { + if (iMqttDeliveryToken.isComplete()) { + if (iMqttDeliveryToken.getMessage() != null) { + String message = iMqttDeliveryToken.getMessage().toString(); + Log.d(TAG, "Message to client [" + client + "] under topic (" + topic + + ") was delivered successfully with the delivery message: '" + message + "'"); + } else { + Log.d(TAG, "Message to client [" + client + "] under topic (" + topic + + ") was delivered successfully."); + } + } else { + Log.w(TAG, "FAILED: Delivery of MQTT message to [" + client + "] under topic [" + topic + "] failed."); + } + } catch (MqttException e) { + Log.w(TAG, "Error occurred whilst trying to read the message from the MQTT delivery token."); + } + } + + /** + * Closes the connection to the MQTT Broker. + */ + public void closeConnection() throws MqttException { + if (client != null && isConnected()) { + client.disconnect(); + } + } + + /** + * Fetches the default options set for the MQTT Client + * + * @return the options that are currently set for the client. + */ + public MqttConnectOptions getOptions() { + return options; + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java new file mode 100644 index 0000000000..88efba9a48 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport; + +/** + * This interface consists of the core functionality related to the transport between any device and the server. The + * interface is an abstraction, regardless of the underlying protocol used for the transport. Implementation of this + * interface by any class that caters a specific protocol (ex: HTTP, XMPP, MQTT, CoAP) would ideally have methods + * specific to the protocol used for communication and other methods that implement the logic related to the devices + * using the protocol. The methods of the interface are identified as generic ones for implementing transport + * protocols for device communication. The implementation can utilize the appropriate method signatures applicable for + * intended protocol. + * + * @param an object of the message type specific to the protocol implemented. To be set to 'String' if there + * isn't anything specific. + */ +public interface TransportHandler { + // a default timeout interval to be used for the protocol specific connections + int DEFAULT_TIMEOUT_INTERVAL = 5000; // millis ~ 5 sec + + /** + * Implements the underlying connect mechanism specific to the protocol enabled by the interface. An object of a + * class that implements this interface would call this method before any communication is started via the + * intended protocol. + */ + void connect(); + + /** + * Used to check whether a connection (via the implemented protocol) to the external-endpoint exists. Ideally + * used to verify that the connection persists and to spawn a reconnection attempt if not. + * + * @return 'true' if connection is already made & exists, else 'false'. + */ + boolean isConnected(); + + /** + * @throws TransportHandlerException in the event of any exceptions that occur whilst processing the message. + * @see TransportHandler#processIncomingMessage(Object, String...) + */ + void processIncomingMessage() throws TransportHandlerException; + + /** + * @param message the message (of the type specific to the protocol) received from the device. + * @throws TransportHandlerException + * @see TransportHandler#processIncomingMessage(Object, String...) + */ + void processIncomingMessage(T message) throws TransportHandlerException; + + /** + * This is an overloaded method with three different method-signatures. This method is used to process any + * incoming messages via the implemented protocol. It would ideally be invoked at a point where a message + * received event is activated (Ex: `MessageArrived` callback in Eclipse-Paho-MQTT Client & `PacketListener`(s) + * in XMPP). + *

+ * + * @param message the message (of the type specific to the protocol) received from the device. + * @param messageParams one or more other parameters received as part-of & relevant-to the message (Ex: MQTT Topic). + * @throws TransportHandlerException in the event of any exceptions that occur whilst processing the message. + */ + void processIncomingMessage(T message, String... messageParams) throws TransportHandlerException; + + /** + * @throws TransportHandlerException in the event of any exceptions that occur whilst sending the message. + * @see TransportHandler#publishDeviceData(String...) + */ + void publishDeviceData() throws TransportHandlerException; + + /** + * @param publishData the message (of the type specific to the protocol) to be sent to the device. + * @throws TransportHandlerException in the event of any exceptions that occur whilst sending the message. + * @see TransportHandler#publishDeviceData(String...) + */ + void publishDeviceData(T publishData) throws TransportHandlerException; + + /** + * This is an overloaded method with three different method-signatures. This method is used to publish messages + * to an external-endpoint/device via the implemented protocol. It could in itself call the (communicating) + * external-endpoint or invoke any method provided by the protocol specific library. + *

+ * + * @param publishData one or more parameters specific to the message and the data to be sent. + * @throws TransportHandlerException in the event of any exceptions that occur whilst sending the message. + */ + void publishDeviceData(String... publishData) throws TransportHandlerException; + + /** + * Implements the underlying disconnect mechanism specific to the protocol enabled by the interface. An object of a + * class that implements this interface would call this method upon completion of all communication. In the case of + * the IoT-Server invoking this would only be required if the server shuts-down. + */ + void disconnect(); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java new file mode 100644 index 0000000000..c52ca2ed0a --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport; + +public class TransportHandlerException extends Exception { + private static final long serialVersionUID = 2736466230451105440L; + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public TransportHandlerException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public TransportHandlerException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public TransportHandlerException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public TransportHandlerException() { + super(); + } + + public TransportHandlerException(Throwable cause) { + super(cause); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java new file mode 100644 index 0000000000..41ff9feffa --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.Calendar; + +/** + * This is a service which triggers to collect + */ +public class SenseScheduleReceiver extends BroadcastReceiver { + private static final int ALARM_INTERVAL = 5000; + @Override + public void onReceive(Context context, Intent intent) { + + AlarmManager service = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(context, SenseService.class); + PendingIntent pending = PendingIntent.getService(context, 0, i, 0); + + Calendar cal = Calendar.getInstance(); + + cal.add(Calendar.SECOND, 30); + service.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), ALARM_INTERVAL, pending); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java new file mode 100644 index 0000000000..69285234f7 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.IBinder; +import org.wso2.carbon.iot.android.sense.event.streams.SenseDataCollector; +import org.wso2.carbon.iot.android.sense.event.streams.battery.BatteryDataReceiver; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; +import org.wso2.carbon.iot.android.sense.util.SenseWakeLock; + +/** + * This service caters to initiate the data collection. + */ +public class SenseService extends Service { + + public static Context context; + + @Override + public void onCreate() { + super.onCreate(); + SenseWakeLock.acquireWakeLock(this); + } + + @Override + public IBinder onBind(Intent arg0) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + context = this; + if (!LocalRegistry.isExist(context)) return Service.START_NOT_STICKY; + //Below triggers the data collection for sensors,location and battery. + SenseDataCollector Sensor = new SenseDataCollector(this, SenseDataCollector.DataType.SENSOR); + SenseDataCollector Location = new SenseDataCollector(this, SenseDataCollector.DataType.LOCATION); + registerReceiver(new BatteryDataReceiver(), new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + + //service will not be stopped until we manually stop the service + return Service.START_NOT_STICKY; + + } + + @Override + public void onDestroy() { + SenseWakeLock.releaseCPUWakeLock(); + super.onDestroy(); + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java new file mode 100644 index 0000000000..1d8e422044 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams; + +/** + * this class extended by each data reader implementation, where the data store logic is implemented in thread. + */ +public abstract class DataReader implements Runnable { +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java new file mode 100644 index 0000000000..3f5de2888f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.Location; + +import java.util.Date; + +/** + * This defines the data structure of the location data that is been collected. + */ +public class LocationData { + private double latitude; // latitude + private double longitude; // longitude + private long timestamp; + + LocationData(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + timestamp = new Date().getTime(); + + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public long getTimeStamp() { + return timestamp; + } + + public void setTimeStamp(long timeStamp) { + timestamp = timeStamp; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java new file mode 100644 index 0000000000..b8d3a4e586 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.Location; + +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.util.Log; +import org.wso2.carbon.iot.android.sense.event.streams.DataReader; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; +import java.util.concurrent.TimeUnit; + +/** + * This is used to retrieve the location data using GPS and used Network connection to increase the accuracy. + */ +public class LocationDataReader extends DataReader implements LocationListener { + protected LocationManager locationManager; + private Context mContext; + private boolean canGetLocation = false; + private static final String TAG = "Location Data"; + + Location location; // location + double latitude; // latitude + double longitude; // longitude + + public LocationDataReader(Context context) { + mContext = context; + getLocation(); + } + + public Location getLocation() { + locationManager = (LocationManager) mContext.getSystemService(mContext.LOCATION_SERVICE); + + // getting GPS status + boolean isGPSEnabled = locationManager + .isProviderEnabled(LocationManager.GPS_PROVIDER); + + // getting network status + boolean isNetworkEnabled = locationManager + .isProviderEnabled(LocationManager.NETWORK_PROVIDER); + + if (!isGPSEnabled && !isNetworkEnabled) { + // no network provider is enabled + } else { + this.canGetLocation = true; + // First get location from Network Provider + if (isNetworkEnabled) { + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, 0, 0, this); + // MIN_TIME_BW_UPDATES, + // MIN_DISTANCE_CHANGE_FOR_UPDATES, this); + + if (locationManager != null) { + location = locationManager + .getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + if (location != null) { + latitude = location.getLatitude(); + longitude = location.getLongitude(); + } + } + } + // if GPS Enabled get lat/long using GPS Services + if (isGPSEnabled) { + if (location == null) { + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, 0, 0, this); + //MIN_TIME_BW_UPDATES, + //MIN_DISTANCE_CHANGE_FOR_UPDATES, this); + + Log.d(TAG, "GPS Enabled"); + if (locationManager != null) { + location = locationManager + .getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (location != null) { + latitude = location.getLatitude(); + longitude = location.getLongitude(); + } + } + } + } + } + return location; + } + + public boolean canGetLocation() { + return this.canGetLocation; + } + + public void stopUsingGPS() { + if (locationManager != null) { + locationManager.removeUpdates(LocationDataReader.this); + } + } + + public double getLatitude() { + if (location != null) { + latitude = location.getLatitude(); + } + // return latitude + return latitude; + } + + /** + * Function to get longitude + */ + public double getLongitude() { + if (location != null) { + longitude = location.getLongitude(); + } + // return longitude + return longitude; + } + + @Override + public void onLocationChanged(Location arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onProviderDisabled(String arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onProviderEnabled(String arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onStatusChanged(String arg0, int arg1, Bundle arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void run() { + Log.d(TAG, "running -Location"); + try { + TimeUnit.MILLISECONDS.sleep(10000); + double lat = getLatitude(); + double longit = getLongitude(); + if (lat != 0 && longit != 0) { + SenseDataHolder.getLocationDataHolder().add(new LocationData(getLatitude(), getLongitude())); + } + } catch (InterruptedException e) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + Log.e(TAG, " Location Data Retrieval Failed"); + } + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java new file mode 100644 index 0000000000..2febb8ba20 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams; + +import android.content.Context; +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationDataReader; +import org.wso2.carbon.iot.android.sense.event.streams.Sensor.SensorDataReader; + +/** + * This class triggered by service to collect the sensor data. + */ +public class SenseDataCollector { + public enum DataType { + SENSOR, LOCATION + } + + public SenseDataCollector(Context ctx, DataType dt) { + DataReader dr = null; + switch (dt) { + case SENSOR: + dr = new SensorDataReader(ctx); + break; + case LOCATION: + dr = new LocationDataReader(ctx); + break; + } + if (dr != null) { + Thread DataCollector = new Thread(dr); + DataCollector.start(); + } + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java new file mode 100644 index 0000000000..abdfeb1555 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams.Sensor; + +import android.hardware.SensorEvent; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; + +import java.util.Date; + +/** + * This defines the data structure of the sensor data that is been collected. + * look at http://developer.android.com/guide/topics/sensors/sensors_overview.html for field description. + */ +public class SensorData { + private int sensorType; + private String sensorName; + private String sensorVendor; + private float sensorValues[]; + private int accuracyStatus; + private long timestamp; + private String collectTimestamp; + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + SensorData(SensorEvent event) { + sensorValues = event.values; + accuracyStatus = event.accuracy; + collectTimestamp = String.valueOf(event.timestamp); + timestamp = new Date().getTime(); + sensorName = supportedSensors.getType(event.sensor.getType()).toUpperCase(); + sensorVendor = event.sensor.getVendor(); + sensorType = event.sensor.getType(); + } + + public int getSensorType() { + return sensorType; + } + + public void setSensorType(int sensorType) { + this.sensorType = sensorType; + } + + public String getSensorName() { + return sensorName; + } + + public void setSensorName(String sensorName) { + this.sensorName = sensorName; + } + + public String getSensorVendor() { + return sensorVendor; + } + + public void setSensorVendor(String sensorVendor) { + this.sensorVendor = sensorVendor; + } + + public float[] getSensorValues() { + return sensorValues; + } + + public void setSensorValues(float sensorValues[]) { + this.sensorValues = sensorValues; + } + + public int getAccuracyStatus() { + return accuracyStatus; + } + + public void setAccuracyStatus(int accuracyStatus) { + this.accuracyStatus = accuracyStatus; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getCollectTimestamp() { + return collectTimestamp; + } + + public void setCollectTimestamp(String collectTimestamp) { + this.collectTimestamp = collectTimestamp; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java new file mode 100644 index 0000000000..4f4697e50c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.Sensor; + +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.event.streams.DataReader; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.TimeUnit; + +/** + * This is used to retrieve the sensor data. + */ +public class SensorDataReader extends DataReader implements SensorEventListener { + private SensorManager mSensorManager; + private Map senseDataStruct = new HashMap<>(); + private Vector sensorVector = new Vector<>(); + Context ctx; + private List sensorList = new ArrayList<>(); + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + public SensorDataReader(Context context) { + ctx = context; + SharedPreferences sharedPreferences = ctx.getSharedPreferences(SupportedSensors.SELECTED_SENSORS, Context + .MODE_MULTI_PROCESS); + Set selectedSet = sharedPreferences.getStringSet(SupportedSensors.SELECTED_SENSORS_BY_USER, null); + mSensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE); + selectedSensorList(selectedSet); + for (Sensor sensor : sensorList) { + mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL); + } + + } + + private void collectSensorData() { + for (Sensor sensor : sensorList) { + try { + if (senseDataStruct.containsKey(sensor.getName())) { + SensorData sensorInfo = senseDataStruct.get(sensor.getName()); + sensorVector.add(sensorInfo); + Log.d(this.getClass().getName(), "Sensor Name " + sensor.getName() + ", Type " + sensor.getType() + " " + + ", sensorValue :" + sensorInfo.getSensorValues()); + } + } catch (Throwable e) { + Log.d(this.getClass().getName(), "error on sensors"); + } + + } + mSensorManager.unregisterListener(this); + } + + public Vector getSensorData() { + try { + TimeUnit.MILLISECONDS.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e(SensorDataReader.class.getName(), e.getMessage()); + } + collectSensorData(); + return sensorVector; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onSensorChanged(SensorEvent event) { + senseDataStruct.put(event.sensor.getName(), new SensorData(event)); + } + + @Override + public void run() { + Log.d(this.getClass().getName(), "running -sensorDataMap"); + Vector sensorDatas = getSensorData(); + for (SensorData data : sensorDatas) { + SenseDataHolder.getSensorDataHolder().add(data); + } + } + + public void selectedSensorList(Set set) { + if (set != null) { + String[] sensorsSet = set.toArray(new String[set.size()]); + for (String s : sensorsSet) { + sensorList.add(mSensorManager.getDefaultSensor(supportedSensors.getType(s.toLowerCase()))); + } + } + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java new file mode 100644 index 0000000000..666e02a85f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams.battery; + +import android.content.Intent; +import android.os.BatteryManager; + +import java.util.Date; + +/** + * This defines the data structure of the battery data that is been collected. + * look at http://developer.android.com/reference/android/os/BatteryManager.html for field description. + */ +public class BatteryData { + + private int health; + private int level; + private int plugged; + private int present; + private int scale; + private int status; + private int temperature; + private int voltage; + private long timestamp; + + BatteryData(Intent intent) { + timestamp = new Date().getTime(); + health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 0); + level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); + plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + present = intent.getExtras().getBoolean(BatteryManager.EXTRA_PRESENT) ? 1 : 0; + scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); + status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 0); + String technology = intent.getExtras().getString(BatteryManager.EXTRA_TECHNOLOGY); + temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); + voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); + + } + + public int getHealth() { + return health; + } + + public void setHealth(int health) { + this.health = health; + } + + public int getLevel() { + return level; + } + + public void setLevel(int level) { + this.level = level; + } + + public int getPlugged() { + return plugged; + } + + public void setPlugged(int plugged) { + this.plugged = plugged; + } + + public int getPresent() { + return present; + } + + public void setPresent(int present) { + this.present = present; + } + + public int getScale() { + return scale; + } + + public void setScale(int scale) { + this.scale = scale; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getTemperature() { + return temperature; + } + + public void setTemperature(int temperature) { + this.temperature = temperature; + } + + public int getVoltage() { + return voltage; + } + + public void setVoltage(int voltage) { + this.voltage = voltage; + } + + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java new file mode 100644 index 0000000000..9fffa1c157 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.battery; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; + +/** + * Whenever the battery level changes This receiver will be triggered. + */ +public class BatteryDataReceiver extends BroadcastReceiver { + + /** + * when the data is retreived then its added to a inmemory map. + * + * @param context of the reciever. + * @param intent of the reciver + */ + @Override + public void onReceive(Context context, Intent intent) { + SenseDataHolder.getBatteryDataHolder().add(new BatteryData(intent)); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java new file mode 100644 index 0000000000..053b7e38ce --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.NavigationView; +import android.support.design.widget.Snackbar; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.RegisterActivity; +import org.wso2.carbon.iot.android.sense.data.publisher.DataPublisherReceiver; +import org.wso2.carbon.iot.android.sense.data.publisher.DataPublisherService; +import org.wso2.carbon.iot.android.sense.event.SenseScheduleReceiver; +import org.wso2.carbon.iot.android.sense.event.SenseService; +import org.wso2.carbon.iot.android.sense.realtimeviewer.datastore.TempStore; +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.RealTimeSensorChangeReceiver; +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor.RealTimeSensorReader; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor.SensorViewAdaptor; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.sensor.selector.SelectSensorDialog; +import org.wso2.carbon.iot.android.sense.speech.detector.WordRecognitionActivity; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +/** + * Activity for selecting sensors available in the device + */ + +public class ActivitySelectSensor extends AppCompatActivity + implements NavigationView.OnNavigationItemSelectedListener, SelectSensorDialog.SensorListListener { + + private SharedPreferences sharedPreferences; + private SelectSensorDialog sensorDialog = new SelectSensorDialog(); + private Set selectedSensorSet = new HashSet<>(); + private ListView listView; + private SensorManager sensorManager; + private ArrayList sensors = new ArrayList<>(); + + private RealTimeSensorReader sensorReader = null; + private RealTimeSensorChangeReceiver realTimeSensorChangeReceiver = new RealTimeSensorChangeReceiver(); + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_activity_select_sensor); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); + + listView = (ListView) findViewById(R.id.senseListContainer); + + registerReceiver(realTimeSensorChangeReceiver, new IntentFilter("sensorDataMap")); + + //Publish data + FloatingActionButton fbtnPublishData = (FloatingActionButton) findViewById(R.id.publish); + + fbtnPublishData.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Snackbar.make(view, "Publishing data started", Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + + DataPublisherReceiver dataPublisherReceiver = new DataPublisherReceiver(); + dataPublisherReceiver.clearAbortBroadcast(); + dataPublisherReceiver.onReceive(getApplicationContext(), null); + } + }); + + FloatingActionButton fbtnAddSensors = (FloatingActionButton) findViewById(R.id.addSensors); + fbtnAddSensors.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + sensorDialog.show(getFragmentManager(), "Sensor List"); + } + }); + + FloatingActionButton fbtnSpeechRecongnizer = (FloatingActionButton) findViewById(R.id.speech); + fbtnSpeechRecongnizer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + EditText sessionIdText = (EditText) findViewById(R.id.sessionId); + String sessionId = sessionIdText.getText().toString(); + if (!sessionId.isEmpty()) { + Intent intent = new Intent(getApplicationContext(), WordRecognitionActivity.class); + intent.putExtra("sessionId", sessionId); + startActivity(intent); + } else { + Toast.makeText(ActivitySelectSensor.this, "Please type a session id value", Toast.LENGTH_SHORT) + .show(); + } + + } + }); + + sharedPreferences = getSharedPreferences(SupportedSensors.SELECTED_SENSORS, 0); + + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + drawer.setDrawerListener(toggle); + toggle.syncState(); + + NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); + navigationView.setNavigationItemSelectedListener(this); + } + + @Override + public void onBackPressed() { + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + if (drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.activity_select_sensor, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_deEnroll) { + + /** + * unregister the sensors and broadcast receivers. + * */ + unregisterSensors(); + unregisterReceivers(); + + if (!LocalRegistry.isExist(getApplicationContext())) { + Intent activity = new Intent(getApplicationContext(), RegisterActivity.class); + startActivity(activity); + } + + LocalRegistry.removeUsername(getApplicationContext()); + LocalRegistry.removeDeviceId(getApplicationContext()); + LocalRegistry.removeServerURL(getApplicationContext()); + LocalRegistry.setExist(false); + //Stop the current running background services. + stopService(new Intent(this, SenseService.class)); //Stop sensor reading service + stopService(new Intent(this, DataPublisherService.class)); //Stop data uploader service + + Intent registerActivity = new Intent(getApplicationContext(), RegisterActivity.class); + registerActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(registerActivity); + finish(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @SuppressWarnings("StatementWithEmptyBody") + @Override + public boolean onNavigationItemSelected(MenuItem item) { + // Handle navigation view item clicks here. + int id = item.getItemId(); + + if (id == R.id.select) { + sensorDialog.show(getFragmentManager(), "Sensor List"); + } + + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + drawer.closeDrawer(GravityCompat.START); + return true; + } + + @Override + public void onDialogPositiveClick(SelectSensorDialog dialog) { + + Log.d("Selected sensors", dialog.getSet().toString()); + selectedSensorSet = dialog.getSet(); + update(); + unregisterSensors(); + + SenseScheduleReceiver senseScheduleReceiver = new SenseScheduleReceiver(); + senseScheduleReceiver.clearAbortBroadcast(); + senseScheduleReceiver.onReceive(this, null); + + /** + * Get the selected sensors + * Register them + * */ + SensorViewAdaptor adaptor1 = new SensorViewAdaptor(getApplicationContext(), TempStore.sensorArrayList); + adaptor1.notifyDataSetChanged(); + + sensorReader = new RealTimeSensorReader(this, adaptor1); + getSensors(); + + for (Sensor s : sensors) { + sensorManager.registerListener(sensorReader, s, SensorManager.SENSOR_DELAY_NORMAL); + } + + realTimeSensorChangeReceiver.updateOnChange(adaptor1); + listView.setAdapter(adaptor1); + + } + + public void update() { + Log.d("Update", "Set the values to Shared Preferences"); + + TempStore.sensorArrayList.clear(); + TempStore.sensorDataMap.clear(); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putStringSet(SupportedSensors.SELECTED_SENSORS_BY_USER, selectedSensorSet); + editor.apply(); + } + + public void getSensors() { + sensors.clear(); + for (String sensor : selectedSensorSet.toArray(new String[selectedSensorSet.size()])) { + sensors.add(sensorManager.getDefaultSensor(supportedSensors.getType(sensor.toLowerCase()))); + } + } + + /** + * This method will unregister all the registered sensors. + */ + public void unregisterSensors() { + if (sensors.size() > 0) { + for (Sensor s : sensors) { + System.out.println(s.getName() + " Unregistered!"); + sensorManager.unregisterListener(sensorReader, s); + } + } + } + + /** + * This method unregisters the real-time broadcast receiver. + */ + public void unregisterReceivers() { + unregisterReceiver(realTimeSensorChangeReceiver); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java new file mode 100644 index 0000000000..2e53438347 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.datastore; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor.RealTimeSensor; + +import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Class to store the real time sensor readings. + */ +public class TempStore { + + /** + * Hash map which is used to store sensor values with the sensor names. + */ + public static ConcurrentMap sensorDataMap = new ConcurrentHashMap<>(); + + /** + * Array List which is used to populate the List view. + */ + public static ArrayList sensorArrayList = new ArrayList<>(); + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java new file mode 100644 index 0000000000..c231ac87ee --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.event; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.datastore.TempStore; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor.SensorViewAdaptor; + +/** + * This class is to detect the sensor change event and update the sensor array list. + * And update the view adaptor which is used to show the sensors list in the Android List view. + */ +public class RealTimeSensorChangeReceiver extends BroadcastReceiver { + + SensorViewAdaptor adaptor; + + public void updateOnChange(SensorViewAdaptor adaptor) { + this.adaptor = adaptor; + } + + @Override + public void onReceive(Context context, Intent intent) { + TempStore.sensorArrayList.clear(); + TempStore.sensorArrayList.addAll(TempStore.sensorDataMap.values()); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java new file mode 100644 index 0000000000..6a71739dc7 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor; + +import android.support.annotation.NonNull; + +/** + * The class to store the sensor data captured by the RealTimeSensorReader. + */ +public class RealTimeSensor implements Comparable { + + + /** + * Name of the sensor. + */ + private String name; + + /** + * The X value reading of the sensor. + */ + private String valueX; + + /** + * The Y value reading of the sensor. + */ + private String valueY; + + /** + * The Y value reading of the sensor. + */ + private String valueZ; + + public RealTimeSensor() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValueX() { + return valueX; + } + + public void setValueX(String valueX) { + this.valueX = valueX; + } + + public String getValueY() { + return valueY; + } + + public void setValueY(String valueY) { + this.valueY = valueY; + } + + public String getValueZ() { + return valueZ; + } + + public void setValueZ(String valueZ) { + this.valueZ = valueZ; + } + + @Override + public String toString() { + return this.valueX + ", " + valueY + ", " + valueZ; + } + + @Override + public int compareTo(@NonNull Object another) { + return this.toString().contains(another.toString()) ? 1 : 0; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java new file mode 100644 index 0000000000..fc4b474e65 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor; + +import android.content.Context; +import android.content.Intent; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.datastore.TempStore; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor.SensorViewAdaptor; + +/** + * This class reads the sensor values in real time. + */ +public class RealTimeSensorReader implements SensorEventListener { + + private Context context; + private SensorViewAdaptor adaptor; + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + public RealTimeSensorReader(Context context, SensorViewAdaptor adaptor) { + this.context = context; + this.adaptor = adaptor; + } + + @Override + public void onSensorChanged(SensorEvent event) { + RealTimeSensor realTimeSensor = new RealTimeSensor(); + realTimeSensor.setName(supportedSensors.getType(event.sensor.getType()).toUpperCase()); + + realTimeSensor.setValueX(event.values[0] + ""); + realTimeSensor.setValueY(event.values[1] + ""); + realTimeSensor.setValueZ(event.values[2] + ""); + + TempStore.sensorDataMap.put(supportedSensors.getType(event.sensor.getType()), realTimeSensor); + + Intent intent = new Intent(); + intent.setAction("sensorDataMap"); + context.sendBroadcast(intent); + + adaptor.notifyDataSetChanged(); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java new file mode 100644 index 0000000000..726c8cded2 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting; + +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.Sensor; +import android.hardware.SensorManager; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class to save the list of sensors that are available in the device, which are supported by the iot server. + * This list will be saved in Shared preferences so that app can use this data when needed. + */ +public class AvailableSensorsInDevice { + + private SharedPreferences sensorPreference; + + /** + * The Android sensor manager which is used to get the sensors available in device. + */ + private SensorManager mSensorManager; + + public AvailableSensorsInDevice(Context context) { + this.sensorPreference = context.getSharedPreferences(SupportedSensors.AVAILABLE_SENSORS, 0); + this.mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + } + + /** + * This method filters the pre defined sensor types from sensors available in device and sets them in Shared + * preferences. + */ + public void setContent() { + SupportedSensors supportedSensors = SupportedSensors.getInstance(); + List sensor_List = supportedSensors.getSensorList(); + Set sensorSet = new HashSet<>(); + List sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL); + + for (String sen : sensor_List) { + if (sensors.contains(mSensorManager.getDefaultSensor(supportedSensors.getType(sen.toLowerCase())))) { + sensorSet.add(sen); + } + } + + SharedPreferences.Editor editor = this.sensorPreference.edit(); + editor.putStringSet(SupportedSensors.GET_AVAILABLE_SENSORS, sensorSet); + editor.apply(); + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java new file mode 100644 index 0000000000..6d57e6d704 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting; + +import android.hardware.Sensor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Class to store the supported sensorDataMap types. + */ +public class SupportedSensors { + + //For set user selected sensors. Will be used by sensorDataMap reading and dialog + public static String SELECTED_SENSORS = "Selected"; + public static String SELECTED_SENSORS_BY_USER = "userSelection"; + + //For setting the available sensors in the device in dialog and AvailableSensorsInDevice + public static String AVAILABLE_SENSORS = "Sensors"; + public static String GET_AVAILABLE_SENSORS = "getAvailableSensors"; + + public static final int SUPPORTED_SENSOR_COUNT = 10; + private static List sensorList = new ArrayList<>(); + private static HashMap sensorTypeMap = new HashMap<>(); + private static HashMap typeSensorMap = new HashMap<>(); + private static SupportedSensors supportedSensors = new SupportedSensors(); + + private SupportedSensors() { + this.setList(); + this.setSensorTypeMap(); + this.setTypeSensorMap(); + } + + public static SupportedSensors getInstance() { + return supportedSensors; + } + + /** + * Set the supported sensor types by the IOT server. + */ + private void setList() { + sensorList.add("Accelerometer"); + sensorList.add("Magnetometer"); + sensorList.add("Gravity"); + sensorList.add("Rotation Vector"); + sensorList.add("Pressure"); + sensorList.add("Light"); + sensorList.add("Gyroscope"); + sensorList.add("Proximity"); + } + + /** + * Populate the hash map which has Sensor name as the key and the sensor type as the value. + */ + private void setSensorTypeMap() { + sensorTypeMap.put("accelerometer", Sensor.TYPE_ACCELEROMETER); + sensorTypeMap.put("magnetometer", Sensor.TYPE_MAGNETIC_FIELD); + sensorTypeMap.put("gravity", Sensor.TYPE_GRAVITY); + sensorTypeMap.put("rotation vector", Sensor.TYPE_GAME_ROTATION_VECTOR); + sensorTypeMap.put("pressure", Sensor.TYPE_PRESSURE); + sensorTypeMap.put("gyroscope", Sensor.TYPE_GYROSCOPE); + sensorTypeMap.put("light", Sensor.TYPE_LIGHT); + sensorTypeMap.put("proximity", Sensor.TYPE_PROXIMITY); + } + + /** + * Populates the hash map which has Sensor type as the key and sensor name as the value. + */ + private void setTypeSensorMap() { + typeSensorMap.put(Sensor.TYPE_ACCELEROMETER, "accelerometer"); + typeSensorMap.put(Sensor.TYPE_MAGNETIC_FIELD, "magnetometer"); + typeSensorMap.put(Sensor.TYPE_GRAVITY, "gravity"); + typeSensorMap.put(Sensor.TYPE_GAME_ROTATION_VECTOR, "rotation vector"); + typeSensorMap.put(Sensor.TYPE_PRESSURE, "pressure"); + typeSensorMap.put(Sensor.TYPE_GYROSCOPE, "gyroscope"); + typeSensorMap.put(Sensor.TYPE_LIGHT, "light"); + typeSensorMap.put(Sensor.TYPE_PROXIMITY, "proximity"); + } + + /** + * Method to get the supported sensor list. + * + * @return the list of sensors supported by the iot server. + */ + public List getSensorList() { + return sensorList; + } + + + /** + * @param sensor The name of the sensor. + * @return The integer representing the type of the sensor, + */ + public int getType(String sensor) { + return sensorTypeMap.get(sensor); + } + + + /** + * @param type The type of the sensor. + * @return The sensor name related to the given sensor type. + */ + public String getType(int type) { + return typeSensorMap.get(type); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java new file mode 100644 index 0000000000..d2d852226d --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor.RealTimeSensor; + +import java.util.List; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +/** + * Adaptor for populate the ListView. + * Takes list of Sensor readings + */ + +//TODO : Add the location and battery data sections. +public class SensorViewAdaptor extends BaseAdapter { + + private Context context; + private List data; + + public SensorViewAdaptor(Context context, List data) { + this.context = context; + this.data = data; + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public Object getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view; + + if (convertView == null) { + view = inflater.inflate(R.layout.display_sensor_values, parent, false); + holder = new ViewHolder(); + holder.name = (TextView) view.findViewById(R.id.name); + holder.valuesX = (TextView) view.findViewById(R.id.X); + holder.valuesY = (TextView) view.findViewById(R.id.Y); + holder.valuesZ = (TextView) view.findViewById(R.id.Z); + view.setTag(holder); + } else { + view = convertView; + holder = (ViewHolder) view.getTag(); + } + + RealTimeSensor data = this.data.get(position); + + holder.name.setText(data.getName()); + holder.valuesX.setText(data.getValueX()); + holder.valuesY.setText(data.getValueY()); + holder.valuesZ.setText(data.getValueZ()); + + return view; + + } + + private class ViewHolder { + public TextView name; + public TextView valuesX; + public TextView valuesY; + public TextView valuesZ; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java new file mode 100644 index 0000000000..5c163c8021 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.view.sensor.selector; + +import android.app.Activity; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Functionality + *

+ * Show the list of available sensors in a list + * Get the user selections + * Put them in to shared preferences + */ + +public class SelectSensorDialog extends DialogFragment { + + protected boolean[] selections = new boolean[SupportedSensors.SUPPORTED_SENSOR_COUNT]; + Activity activity; + SensorListListener sensorListListener; + private Set selectedSensorSet = new HashSet<>(); + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle("Select Sensors"); + activity = getActivity(); + + SharedPreferences preferences = getActivity().getSharedPreferences(SupportedSensors.AVAILABLE_SENSORS, Context. + MODE_MULTI_PROCESS); + + Set set = preferences.getStringSet(SupportedSensors.GET_AVAILABLE_SENSORS, null); + final CharSequence[] sequence = getSequence(set); + + final boolean[] pos = new boolean[selections.length]; + final boolean[] neg = new boolean[selections.length]; + + builder.setMultiChoiceItems(sequence, selections, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + selectedSensorSet.add(sequence[which].toString()); + + pos[which] = true; + } else { + selectedSensorSet.remove(sequence[which].toString()); + neg[which] = true; + } + } + }); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d("Click", "Ok"); + //call sensorDataMap reading class + sensorListListener.onDialogPositiveClick(SelectSensorDialog.this); + + } + }); + + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d("Click", "Cancel"); + for (int i = 0; i < SupportedSensors.SUPPORTED_SENSOR_COUNT; i++) { + + if (pos[i]) + selections[i] = false; + if (neg[i]) + selections[i] = true; + } + } + }); + + return builder.create(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onDetach() { + super.onDetach(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + sensorListListener = (SensorListListener) getActivity(); + } catch (ClassCastException ex) { + throw new ClassCastException(activity.toString() + " must implement the SensorListener"); + } + } + + @Override + public void onStart() { + super.onStart(); + } + + /** + * Interface to be implemented by the parent + */ + public CharSequence[] getSequence(Set sensorset) { + CharSequence[] seq; + String[] seq2 = sensorset.toArray(new String[sensorset.size()]); + seq = Arrays.copyOf(seq2, seq2.length); + return seq; + } + + public Set getSet() { + return this.selectedSensorSet; + } + + public interface SensorListListener { + void onDialogPositiveClick(SelectSensorDialog dialog); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java new file mode 100644 index 0000000000..83b702d456 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector; + +/** + * This interface is used to retrieve the voice commands and restart the listenting service + */ +public interface IVoiceControl { + /** + * This will be executed when a voice command was found + * @param voiceCommands + */ + public abstract void processVoiceCommands(String... voiceCommands); + + /** + * This will be executed after a voice command was processed to keep the recognition service activated + */ + public void restartListeningService(); + + public void finish(); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java new file mode 100644 index 0000000000..903b6c3a8e --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java @@ -0,0 +1,132 @@ +package org.wso2.carbon.iot.android.sense.speech.detector; + +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.realtimeviewer.ActivitySelectSensor; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ListeningActivity; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ProcessWords; +import org.wso2.carbon.iot.android.sense.speech.detector.util.VoiceRecognitionListener; +import org.wso2.carbon.iot.android.sense.speech.detector.util.WordData; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +public class WordRecognitionActivity extends ListeningActivity { + Button setThreasholdButton; + Button addWordButton; + Button removeWordButton; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_speech_sense_main); + context = getApplicationContext(); // Needs to be set + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + VoiceRecognitionListener.getInstance().setListener(this); // Here we set the current listener + addListenerOnSetThreasholdButton(); + addListenerOnAddWordButton(); + addListenerOnRemoveWordButton(); + String sessionId = getIntent().getStringExtra("sessionId"); + ProcessWords.setSessionId(sessionId); + FloatingActionButton fbtnSpeechRecongnizer = (FloatingActionButton) findViewById(R.id.sensorChange); + fbtnSpeechRecongnizer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + WordData wordData = new WordData(ProcessWords.getSessionId(), SenseConstants.EVENT_LISTENER_FINISHED, 1); + SenseDataHolder.getWordDataHolder().add(wordData); + stopListening(); + Intent intent = new Intent(getApplicationContext(), ActivitySelectSensor.class); + startActivity(intent); + } + }); + Long tsLong = System.currentTimeMillis() / 1000; + WordData wordData = new WordData(sessionId, SenseConstants.EVENT_LISTENER_STARTED, 1); + SenseDataHolder.getWordDataHolder().add(wordData); + startListening(); // starts listening + } + + @Override + public void onBackPressed() { + } + + @Override + public void processVoiceCommands(String... voiceCommands) { + if(voiceCommands==null || voiceCommands.length==0){ + return; + } + ProcessWords processWords = new ProcessWords(this); + processWords.execute(voiceCommands); + restartListeningService(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + // getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_deEnroll) { + return true; + } + return super.onOptionsItemSelected(item); + } + + public void addListenerOnSetThreasholdButton() { + setThreasholdButton = (Button) findViewById(R.id.setThreshold); + setThreasholdButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + String thresholdString = ((EditText) findViewById(R.id.editThreashold)).getText().toString(); + try{ + ProcessWords.setThreshold(Integer.parseInt(thresholdString)); + } catch (NumberFormatException e) { + Toast.makeText(WordRecognitionActivity.this, "Invalid Threshold - " + thresholdString, Toast.LENGTH_SHORT).show(); + } + } + }); + } + + public void addListenerOnAddWordButton() { + addWordButton = (Button) findViewById(R.id.addWord); + addWordButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + String word = ((EditText) findViewById(R.id.wordText)).getText().toString(); + ProcessWords.addWord(word); + Toast.makeText(WordRecognitionActivity.this, word + " is added to the list", Toast.LENGTH_SHORT).show(); + } + }); + } + + public void addListenerOnRemoveWordButton() { + removeWordButton = (Button) findViewById(R.id.removeWord); + removeWordButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + String word = ((EditText) findViewById(R.id.wordText)).getText().toString(); + Toast.makeText(WordRecognitionActivity.this, word + " is removed from the list", Toast.LENGTH_SHORT).show(); + ProcessWords.removeWord(word); + } + + }); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java new file mode 100644 index 0000000000..db37583b58 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.speech.detector.IVoiceControl; + +/** + * This Activity Contols the Speech Recognizer Activity. + */ +public abstract class ListeningActivity extends Activity implements IVoiceControl { + + protected SpeechRecognizer sr; + protected Context context; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Starts the listening service. + */ + protected void startListening() { + initSpeech(); + Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + sr.startListening(intent); + } + + /** + * Stop the listening service. + */ + protected void stopListening() { + if (sr != null) { + sr.stopListening(); + sr.cancel(); + sr.destroy(); + } + sr = null; + } + + /** + * Initialize the speech. + */ + protected void initSpeech() { + if (sr == null) { + sr = SpeechRecognizer.createSpeechRecognizer(this); + if (!SpeechRecognizer.isRecognitionAvailable(context)) { + Toast.makeText(context, "Speech Recognition is not available", + Toast.LENGTH_LONG).show(); + finish(); + } + sr.setRecognitionListener(VoiceRecognitionListener.getInstance()); + } + } + + @Override + public void finish() { + stopListening(); + super.finish(); + } + + @Override + protected void onStop() { + stopListening(); + super.onStop(); + } + + @Override + protected void onDestroy() { + if (sr != null) { + sr.stopListening(); + sr.cancel(); + sr.destroy(); + } + super.onDestroy(); + } + + @Override + protected void onPause() { + if(sr!=null){ + sr.stopListening(); + sr.cancel(); + sr.destroy(); + + } + sr = null; + + super.onPause(); + } + + /** + * Is abstract so the inheriting classes need to implement it. Here you put your code which should be executed once + * a command was found + */ + @Override + public abstract void processVoiceCommands(String ... voiceCommands); + + @Override + public void restartListeningService() { + stopListening(); + startListening(); + } +} + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java new file mode 100644 index 0000000000..9933904a57 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +import android.app.Activity; +import android.os.AsyncTask; +import android.widget.EditText; + +import org.apache.commons.codec.language.Soundex; +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationData; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.tartarus.snowball.ext.englishStemmer; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +/** + * This class process the words form required words with the recongnized words to check whether it matches with the + * certain threshold. + */ +public class ProcessWords extends AsyncTask { + private static volatile double threshold = 80; + private static volatile Map wordDataMap = new ConcurrentHashMap<>(); + private static String sessionId = "default"; + private static Soundex soundex = new Soundex(); + Activity activity; + + public ProcessWords(Activity activity) { + this.activity = activity; + } + + /** + * Add the list of words which are used for reference. + * @param wordlist that needs to be looked upon in the speech + */ + public static void addWords(List wordlist) { + for (String word : wordlist) { + if (!wordDataMap.keySet().contains(word) && !word.isEmpty()) { + wordDataMap.put(word, new WordData(sessionId, word, 0)); + } + } + } + + /** + * Process the recognized word list. + * @param voiceCommands word lists. + */ + private void processTexts(String... voiceCommands) { + for (String requiredWord : wordDataMap.keySet()) { + int maxOccurunce = 0; + for (String command : voiceCommands) { + int occurence = 0; + for (String word : command.split(" ")) { + if (StringSimilarity.similarity(requiredWord, word) > threshold) { + occurence++; + continue; + } + if (soundex.encode(requiredWord).equals(soundex.encode(word))) { + occurence++; + continue; + } + if (StringSimilarity.similarity(requiredWord, stem(word)) > threshold) { + occurence++; + continue; + } + } + if (maxOccurunce < occurence) { + maxOccurunce = occurence; + } + } + if (maxOccurunce > 0) { + WordData wordData = wordDataMap.get(requiredWord); + wordData.addOccurences(maxOccurunce); + wordDataMap.put(requiredWord, wordData); + } + } + } + + /** + * Check for distance between the referenced and recognized words. + * @param params + * @return + */ + @Override + protected String doInBackground(String... params) { + processTexts(params); + publishProgress(); + return ""; + } + + /** + * update it in the UI. + * @param values words list. + */ + @Override + protected void onProgressUpdate(Void... values) { + super.onProgressUpdate(values); + EditText content = (EditText) activity.findViewById(R.id.command); + String text = ""; + for (String key : ProcessWords.wordDataMap.keySet()) { + text = text + key + " - " + ProcessWords.wordDataMap.get(key).getOccurences() + "\n"; + } + content.setText(text); + EditText thresholdText = (EditText) activity.findViewById(R.id.editThreashold); + thresholdText.setText("" + threshold); + } + + /** + * set the threshold to determine the distance. + * @param threshold to determine the distance. + */ + public static synchronized void setThreshold(int threshold) { + ProcessWords.threshold = threshold; + } + + /** + * + * @param sessionId for each listening session. + */ + public static synchronized void setSessionId(String sessionId) { + ProcessWords.sessionId = sessionId; + } + + /** + * + * retrieve sessionId for each listening session. + */ + public static synchronized String getSessionId() { + return ProcessWords.sessionId; + } + + /** + * @param word that is used for refrerence. + */ + public static synchronized void addWord(String word) { + if (!wordDataMap.keySet().contains(word) && !word.isEmpty()) { + wordDataMap.put(word, new WordData(sessionId, word, 0)); + } + } + + /** + * + * @param word that needs to be removed from the reference list. + */ + public static synchronized void removeWord(String word) { + cleanAndPushToWordMap(); + wordDataMap.remove(word); + } + + /** + * clean in memory content and pubish it to the server. + */ + public static synchronized void cleanAndPushToWordMap() { + for (String word : wordDataMap.keySet()) { + WordData wordData = wordDataMap.get(word); + SenseDataHolder.getWordDataHolder().add(wordData); + wordDataMap.put(word, new WordData(sessionId, word, 0)); + } + } + + /** + * apply porter stem algorithm + * @param word to be stemmed. + * @return + */ + private static String stem(String word) + { + englishStemmer stemmer = new englishStemmer(); + stemmer.setCurrent(word); + boolean result = stemmer.stem(); + if (!result) + { + return word; + } + return stemmer.getCurrent(); + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java new file mode 100644 index 0000000000..a44e304f42 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +/** + * Calculates the similarity of strings. + */ +public class StringSimilarity { + + /** + * Calculates the similarity (a number within 0 and 1) between two strings. + */ + public static double similarity(String s1, String s2) { + String longer = s1, shorter = s2; + if (s1.length() < s2.length()) { // longer should always have greater length + longer = s2; + shorter = s1; + } + int longerLength = longer.length(); + if (longerLength == 0) { + return 1.0; /* both strings are zero length */ + } + return ((longerLength - editDistance(longer, shorter)) / (double) longerLength) * 100; + } + + // Example implementation of the Levenshtein Edit Distance + // See http://rosettacode.org/wiki/Levenshtein_distance#Java + private static int editDistance(String s1, String s2) { + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + int[] costs = new int[s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) { + int lastValue = i; + for (int j = 0; j <= s2.length(); j++) { + if (i == 0) + costs[j] = j; + else { + if (j > 0) { + int newValue = costs[j - 1]; + if (s1.charAt(i - 1) != s2.charAt(j - 1)) + newValue = Math.min(Math.min(newValue, lastValue), + costs[j]) + 1; + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + } + if (i > 0) + costs[s2.length()] = lastValue; + } + return costs[s2.length()]; + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java new file mode 100644 index 0000000000..52ce8d52ff --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +import android.os.Bundle; +import android.speech.RecognitionListener; +import android.speech.SpeechRecognizer; +import org.wso2.carbon.iot.android.sense.speech.detector.IVoiceControl; +import java.util.ArrayList; + +/** + * This triggers android voice recognition listener. + */ +public class VoiceRecognitionListener implements RecognitionListener { + private static VoiceRecognitionListener instance = null; + + IVoiceControl listener; // This is your running activity (we will initialize it later) + + public static VoiceRecognitionListener getInstance() { + if (instance == null) { + instance = new VoiceRecognitionListener(); + } + return instance; + } + + private VoiceRecognitionListener() { } + + public void setListener(IVoiceControl listener) { + this.listener = listener; + } + + public void processVoiceCommands(String... voiceCommands) { + listener.processVoiceCommands(voiceCommands); + } + + // This method will be executed when voice commands were found + public void onResults(Bundle data) { + ArrayList matches = data.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + String[] commands = new String[matches.size()]; + commands = matches.toArray(commands); + processVoiceCommands(commands); + } + + // User starts speaking + public void onBeginningOfSpeech() { + System.out.println("Starting to listen"); + } + + public void onBufferReceived(byte[] buffer) { } + + // User finished speaking + public void onEndOfSpeech() { + System.out.println("Waiting for result..."); + } + + // If the user said nothing the service will be restarted + public void onError(int error) { + if (listener != null) { + listener.restartListeningService(); + } + } + public void onEvent(int eventType, Bundle params) { } + + public void onPartialResults(Bundle partialResults) { } + + public void onReadyForSpeech(Bundle params) { } + + public void onRmsChanged(float rmsdB) { } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java new file mode 100644 index 0000000000..fb76eab24f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java @@ -0,0 +1,47 @@ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +/** + * This defines the data structure of the word data. + */ +public class WordData { + /** + * timestamp for all the occurences + */ + private long timestamp; + private int occurences; + private String word; + private String sessionId; + + public WordData(String sessionId, String word, int occurences) { + this.timestamp = System.currentTimeMillis() / 1000; + this.occurences = occurences; + this.word = word; + this.sessionId = sessionId; + } + + public long getTimestamp() { + return timestamp; + } + + public int getOccurences() { + return occurences; + } + + public String getWord() { + return word; + } + + public String getSessionId() { + return sessionId; + } + + /** + * @param occurences for the word and then add the timestamp for each occurences. + */ + public void addOccurences(int occurences) { + this.occurences = this.occurences + occurences; + this.timestamp = System.currentTimeMillis() / 1000; + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java new file mode 100644 index 0000000000..e9d3d430f7 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * This is used to store the values in either in memory or in shared preferences. + */ +public class LocalRegistry { + + private static final String SENSE_SHARED_PREFERENCES = "senseSharedPreferences"; + private static final String USERNAME_KEY = "usernameKey"; + private static final String DEVICE_ID_KEY = "deviceIdKey"; + private static final String SERVER_HOST_KEY = "serverHostKey"; + private static final String ACCESS_TOKEN_KEY = "accessTokenKey"; + private static final String REFRESH_TOKEN_KEY = "refreshTokenKey"; + private static final String MQTT_PORT_KEY = "mqttPort"; + private static boolean exists = false; + private static String username; + private static String deviceId; + private static String serverURL; + private static MQTTTransportHandler mqttTransportHandler; + private static String accessToken; + private static String refreshToken; + private static int mqttPort; + + public static boolean isExist(Context context) { + if (!exists) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + String username = sharedpreferences.getString(USERNAME_KEY, ""); + String deviceId = sharedpreferences.getString(DEVICE_ID_KEY, ""); + exists = (username != null && !username.isEmpty() && deviceId != null && !deviceId.isEmpty()); + } + return exists; + } + + public static void setExist(boolean status) { + exists = status; + } + + + public static void addUsername(Context context, String username) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(USERNAME_KEY, username); + editor.commit(); + LocalRegistry.username = username; + } + + public static String getUsername(Context context) { + if (LocalRegistry.username == null || username.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.username = sharedpreferences.getString(USERNAME_KEY, ""); + } + return LocalRegistry.username; + } + + public static void removeUsername(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.clear(); + editor.remove(USERNAME_KEY); + editor.commit(); + LocalRegistry.username = null; + } + + public static void addDeviceId(Context context, String deviceId) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(DEVICE_ID_KEY, deviceId); + editor.commit(); + LocalRegistry.deviceId = deviceId; + } + + public static void removeDeviceId(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(DEVICE_ID_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.deviceId = null; + } + + public static String getDeviceId(Context context) { + if (LocalRegistry.deviceId == null || LocalRegistry.deviceId.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.deviceId = sharedpreferences.getString(DEVICE_ID_KEY, ""); + } + return LocalRegistry.deviceId; + } + + public static void addServerURL(Context context, String host) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(SERVER_HOST_KEY, host); + editor.commit(); + LocalRegistry.serverURL = host; + } + + public static void removeServerURL(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(SERVER_HOST_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.serverURL = null; + } + + public static String getServerURL(Context context) { + if (LocalRegistry.serverURL == null || LocalRegistry.serverURL.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.serverURL = sharedpreferences.getString(SERVER_HOST_KEY, ""); + } + return LocalRegistry.serverURL; + } + + public static void addAccessToken(Context context, String accessToken) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(ACCESS_TOKEN_KEY, accessToken); + editor.commit(); + LocalRegistry.accessToken = accessToken; + } + + public static void removeAccessToken(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(ACCESS_TOKEN_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.accessToken = null; + } + + public static String getAccessToken(Context context) { + if (LocalRegistry.accessToken == null || LocalRegistry.accessToken.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.accessToken = sharedpreferences.getString(ACCESS_TOKEN_KEY, ""); + } + return LocalRegistry.accessToken; + } + + public static void addRefreshToken(Context context, String refreshToken) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(REFRESH_TOKEN_KEY, refreshToken); + editor.commit(); + LocalRegistry.refreshToken = refreshToken; + } + + public static void removeRefreshToken(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(REFRESH_TOKEN_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.refreshToken = null; + } + + public static String getRefreshToken(Context context) { + if (LocalRegistry.refreshToken == null || LocalRegistry.refreshToken.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.refreshToken = sharedpreferences.getString(REFRESH_TOKEN_KEY, ""); + } + return LocalRegistry.refreshToken; + } + + public static void addMqttPort(Context context, int port) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putInt(MQTT_PORT_KEY, port); + editor.commit(); + LocalRegistry.mqttPort = port; + } + + public static void removeMqttPort(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(MQTT_PORT_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.mqttPort = 0; + } + + public static int getMqttPort(Context context) { + if (LocalRegistry.mqttPort == 0) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.mqttPort = sharedpreferences.getInt(MQTT_PORT_KEY, SenseConstants.MQTT_BROKER_PORT); + } + return LocalRegistry.mqttPort; + } + + public static String getServerHost(Context context) { + + URL url = null; + String urlString = getServerURL(context); + try { + url = new URL(urlString); + return url.getHost(); + } catch (MalformedURLException e) { + Log.e("Host ", "Invalid urlString :" + urlString); + return null; + } + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java new file mode 100644 index 0000000000..173525b825 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.Context; +import android.os.Build; +import android.util.Log; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; + +import java.util.Map; +import java.util.concurrent.ExecutionException; + +/** + * This Client is used for http communication with the server. + */ +public class SenseClient { + private final static String TAG = "SenseService Client"; + + private Context context; + + public SenseClient(Context context) { + this.context = context; + } + + /** + * Enroll the device. + * + * @param username + * @param password + * @param deviceId + * @return + */ + public boolean register(String username, String password, String deviceId) { + Map response = registerWithTimeWait(username, password, deviceId); + String responseStatus = response.get("status"); + if (responseStatus.trim().contains(SenseConstants.Request.REQUEST_SUCCESSFUL)) { + Toast.makeText(context, "Device Registered", Toast.LENGTH_LONG).show(); + return true; + } else if (responseStatus.trim().contains(SenseConstants.Request.REQUEST_CONFLICT)) { + Toast.makeText(context, "Login Successful", Toast.LENGTH_LONG).show(); + return true; + } else { + Toast.makeText(context, "Authentication failed, please check your credentials and try again.", Toast + .LENGTH_LONG).show(); + + return false; + } + } + + public Map registerWithTimeWait(String username, String password, String deviceId) { + for (int i = 1; i <= SenseConstants.Request.MAX_ATTEMPTS; i++) { + Log.d(TAG, "Attempt #" + i + " to register"); + try { + SenseClientAsyncExecutor senseClientAsyncExecutor = new SenseClientAsyncExecutor(context); + String endpoint = LocalRegistry.getServerURL(context); + senseClientAsyncExecutor.execute(username, password, deviceId, endpoint); + Map response = senseClientAsyncExecutor.get(); + if (response != null) { + return response; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e("Send Sensor Data", "Thread Interruption for endpoint " + LocalRegistry.getServerURL(context)); + } catch (ExecutionException e) { + Log.e("Send Sensor Data", "Failed to push data to the endpoint " + LocalRegistry.getServerURL(context)); + } + } + return null; + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java new file mode 100644 index 0000000000..56842f6b2b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Build; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.util.dto.AccessTokenInfo; +import org.wso2.carbon.iot.android.sense.util.dto.AndroidSenseManagerService; +import org.wso2.carbon.iot.android.sense.util.dto.ApiApplicationRegistrationService; +import org.wso2.carbon.iot.android.sense.util.dto.ApiRegistrationProfile; +import org.wso2.carbon.iot.android.sense.util.dto.DynamicClientRegistrationService; +import org.wso2.carbon.iot.android.sense.util.dto.OAuthApplicationInfo; +import org.wso2.carbon.iot.android.sense.util.dto.OAuthRequestInterceptor; +import org.wso2.carbon.iot.android.sense.util.dto.RegistrationProfile; +import org.wso2.carbon.iot.android.sense.util.dto.TokenIssuerService; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import feign.Client; +import feign.Feign; +import feign.FeignException; +import feign.auth.BasicAuthRequestInterceptor; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.jaxrs.JAXRSContract; + +public class SenseClientAsyncExecutor extends AsyncTask> { + + private final static String TAG = "SenseService Client"; + private static final String STATUS = "status"; + private final static String DEVICE_NAME = Build.MANUFACTURER + " " + Build.MODEL; + private Context context; + + public SenseClientAsyncExecutor(Context context) { + this.context = context; + + } + + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + } + }; + + Client disableHostnameVerification = new Client.Default(getTrustedSSLSocketFactory(), new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }); + + @Override + protected Map doInBackground(String... parameters) { + if (android.os.Debug.isDebuggerConnected()) + android.os.Debug.waitForDebugger(); + String response; + Map response_params = new HashMap<>(); + String username = parameters[0]; + String password = parameters[1]; + String deviceId = parameters[2]; + String endpoint = parameters[3]; + Map responseMap = new HashMap<>(); + responseMap.put(STATUS, "200"); + try { + //DynamicClientRegistraiton. + DynamicClientRegistrationService dynamicClientRegistrationService = Feign.builder() + .client(disableHostnameVerification).contract(new + JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(DynamicClientRegistrationService.class, endpoint + SenseConstants.DCR_CONTEXT); + RegistrationProfile registrationProfile = new RegistrationProfile(); + String applicationName = "android-sense:" + deviceId; + registrationProfile.setOwner(username); + registrationProfile.setClientName(applicationName); + registrationProfile.setCallbackUrl(""); + registrationProfile.setGrantType("password refresh_token client_credentials"); + registrationProfile.setApplicationType("device"); + registrationProfile.setTokenScope("production"); + OAuthApplicationInfo oAuthApplicationInfo = dynamicClientRegistrationService.register(registrationProfile); + + //PasswordGrantType + TokenIssuerService tokenIssuerService = Feign.builder().client(disableHostnameVerification).requestInterceptor( + new BasicAuthRequestInterceptor(oAuthApplicationInfo.getClient_id(), oAuthApplicationInfo.getClient_secret())) + .contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(TokenIssuerService.class, endpoint + SenseConstants.TOKEN_ISSUER_CONTEXT); + AccessTokenInfo accessTokenInfo = tokenIssuerService.getToken("password", username, password); + + //ApiApplicationRegistration + ApiApplicationRegistrationService apiApplicationRegistrationService = Feign.builder().client(disableHostnameVerification) + .requestInterceptor(new OAuthRequestInterceptor(accessTokenInfo.getAccess_token())) + .contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(ApiApplicationRegistrationService.class, endpoint + SenseConstants.API_APPLICATION_REGISTRATION_CONTEXT); + ApiRegistrationProfile apiRegistrationProfile = new ApiRegistrationProfile(); + apiRegistrationProfile.setApplicationName(applicationName); + apiRegistrationProfile.setConsumerKey(oAuthApplicationInfo.getClient_id()); + apiRegistrationProfile.setConsumerSecret(oAuthApplicationInfo.getClient_secret()); + apiRegistrationProfile.setIsAllowedToAllDomains(false); + apiRegistrationProfile.setIsMappingAnExistingOAuthApp(true); + apiRegistrationProfile.setTags(new String[]{SenseConstants.DEVICE_TYPE}); + String replyMsg = apiApplicationRegistrationService.register(apiRegistrationProfile); + + //DeviceRegister + AndroidSenseManagerService androidSenseManagerService = Feign.builder().client(disableHostnameVerification) + .requestInterceptor(new OAuthRequestInterceptor(accessTokenInfo.getAccess_token())) + .contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(AndroidSenseManagerService.class, "https://192.168.56.1:8243" + SenseConstants.REGISTER_CONTEXT); + boolean registered = androidSenseManagerService.register(deviceId, DEVICE_NAME); + if (registered) { + LocalRegistry.addAccessToken(context, accessTokenInfo.getAccess_token()); + LocalRegistry.addRefreshToken(context, accessTokenInfo.getRefresh_token()); + } + return responseMap; + } catch (FeignException e) { + responseMap.put(STATUS, "" + e.status()); + return responseMap; + } + } + + private SSLSocketFactory getTrustedSSLSocketFactory() { + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + return sc.getSocketFactory(); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + Log.e(SenseClientAsyncExecutor.class.getName(), "Invalid Certificate"); + return null; + } + + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java new file mode 100644 index 0000000000..463cf8ab9f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationData; +import org.wso2.carbon.iot.android.sense.event.streams.Sensor.SensorData; +import org.wso2.carbon.iot.android.sense.event.streams.battery.BatteryData; +import org.wso2.carbon.iot.android.sense.speech.detector.util.WordData; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This holds the sensor,battery and location data inmemory. + */ +public class SenseDataHolder { + + private static List sensorDataHolder; + private static List batteryDataHolder; + private static List locationDataHolder; + private static List wordDataHolder; + + public static List getSensorDataHolder(){ + if(sensorDataHolder == null){ + sensorDataHolder = new CopyOnWriteArrayList<>(); + } + return sensorDataHolder; + } + + public static List getBatteryDataHolder(){ + if(batteryDataHolder == null){ + batteryDataHolder = new CopyOnWriteArrayList<>(); + } + return batteryDataHolder; + } + + public static List getLocationDataHolder(){ + if(locationDataHolder == null){ + locationDataHolder = new CopyOnWriteArrayList<>(); + } + return locationDataHolder; + } + + public static List getWordDataHolder(){ + if(wordDataHolder == null){ + wordDataHolder = new CopyOnWriteArrayList<>(); + } + return wordDataHolder; + } + + public static void resetSensorDataHolder(){ + sensorDataHolder = null; + } + + public static void resetBatteryDataHolder(){ + batteryDataHolder = null; + } + + public static void resetLocationDataHolder(){ + locationDataHolder = null; + } + + public static void resetWordDataHolder() { + wordDataHolder = null; + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java new file mode 100644 index 0000000000..956e780def --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.ContentResolver; +import android.content.Context; +import android.telephony.TelephonyManager; + +import java.util.UUID; + +public class SenseUtils { + + /** + * this generate the device Id + * + * @param baseContext + * @param contentResolver + * @return + */ + //http://stackoverflow.com/questions/2785485/is-there-a-unique-android-device-id + public static String generateDeviceId(Context baseContext, ContentResolver contentResolver) { + + final TelephonyManager tm = (TelephonyManager) baseContext.getSystemService(Context.TELEPHONY_SERVICE); + + final String tmDevice, tmSerial, androidId; + tmDevice = String.valueOf(tm.getDeviceId()); + tmSerial = String.valueOf(tm.getSimSerialNumber()); + androidId = String.valueOf(android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID)); + + UUID deviceUuid = new UUID(androidId.hashCode(), ((long) tmDevice.hashCode() << 32) | tmSerial.hashCode()); + return deviceUuid.toString(); + + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java new file mode 100644 index 0000000000..1a6a36bbbe --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +/** + * It doesnt let the phone goto sleep. + */ +public class SenseWakeLock { + + private static PowerManager.WakeLock wakeLock; + private static String TAG = "Wake Lock"; + + public static void acquireWakeLock(Context context) { + Log.i(SenseWakeLock.class.getSimpleName(), "Acquire CPU wakeup lock start"); + if (wakeLock == null) { + Log.i(TAG, "CPU wakeUp log is not null"); + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SenseWakeLock"); + } + wakeLock.acquire(); + } + + public static void releaseCPUWakeLock() { + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + } + Log.i(TAG, "Release wakeup"); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java new file mode 100644 index 0000000000..65170970bf --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java @@ -0,0 +1,40 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +public class AccessTokenInfo { + public String token_type; + public String expires_in; + public String refresh_token; + public String access_token; + + public String getToken_type() { + return token_type; + } + + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + public String getExpires_in() { + return expires_in; + } + + public void setExpires_in(String expires_in) { + this.expires_in = expires_in; + } + + public String getRefresh_token() { + return refresh_token; + } + + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java new file mode 100644 index 0000000000..0cbf26bd28 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java @@ -0,0 +1,13 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; + +public interface AndroidSenseManagerService { + + @Path("devices/{device_id}") + @POST + boolean register(@PathParam("device_id") String deviceId, @QueryParam("deviceName") String deviceName); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java new file mode 100644 index 0000000000..e8cd26148b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java @@ -0,0 +1,25 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * This is the application registration service that exposed for apimApplicationRegistration + */ + +@Path("/register") +public interface ApiApplicationRegistrationService { + + /** + * This method is used to register api application + * + * @param registrationProfile contains the necessary attributes that are needed in order to register an app. + */ + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + String register(ApiRegistrationProfile registrationProfile); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java new file mode 100644 index 0000000000..433ef707b8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java @@ -0,0 +1,64 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + + +/** + * This class represents the data that are required to register + * the oauth application. + */ +public class ApiRegistrationProfile { + + public String applicationName; + public String tags[]; + public boolean isAllowedToAllDomains; + public String consumerKey; + public String consumerSecret; + public boolean isMappingAnExistingOAuthApp; + + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + public String[] getTags() { + return tags; + } + + public void setTags(String[] tags) { + this.tags = tags; + } + + public boolean isAllowedToAllDomains() { + return isAllowedToAllDomains; + } + + public void setIsAllowedToAllDomains(boolean isAllowedToAllDomains) { + this.isAllowedToAllDomains = isAllowedToAllDomains; + } + + public boolean isMappingAnExistingOAuthApp() { + return isMappingAnExistingOAuthApp; + } + + public void setIsMappingAnExistingOAuthApp(boolean isMappingAnExistingOAuthApp) { + this.isMappingAnExistingOAuthApp = isMappingAnExistingOAuthApp; + } + + public String getConsumerKey() { + return consumerKey; + } + + public void setConsumerKey(String consumerKey) { + this.consumerKey = consumerKey; + } + + public String getConsumerSecret() { + return consumerSecret; + } + + public void setConsumerSecret(String consumerSecret) { + this.consumerSecret = consumerSecret; + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java new file mode 100644 index 0000000000..aa409673a8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * you may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/register") +public interface DynamicClientRegistrationService { + + /** + * This method is used to register an Oauth application. + * + * @param profile contains the necessary attributes that are + * needed in order to register an app. + * @return Status 200 if success including consumerKey and consumerSecret. + */ + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + OAuthApplicationInfo register(RegistrationProfile profile); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java new file mode 100644 index 0000000000..4bf5bf354f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java @@ -0,0 +1,44 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +/** + * This class represents an OAuth application populated with necessary data. + */ +public class OAuthApplicationInfo { + + public String client_id; + public String client_name; + public String callback_url; + public String client_secret; + + public String getClient_id() { + return client_id; + } + + public void setClient_id(String client_id) { + this.client_id = client_id; + } + + public String getClient_name() { + return client_name; + } + + public void setClient_name(String client_name) { + this.client_name = client_name; + } + + public String getCallback_url() { + return callback_url; + } + + public void setCallback_url(String callback_url) { + this.callback_url = callback_url; + } + + public String getClient_secret() { + return client_secret; + } + + public void setClient_secret(String client_secret) { + this.client_secret = client_secret; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java new file mode 100644 index 0000000000..32aac77c98 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java @@ -0,0 +1,26 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + + +import feign.RequestInterceptor; +import feign.RequestTemplate; + +import static feign.Util.checkNotNull; + +public class OAuthRequestInterceptor implements RequestInterceptor { + + private final String headerValue; + + /** + * Creates an interceptor that authenticates all requests with the specified OAUTH token + * + * @param token the access token to use for authentication + */ + public OAuthRequestInterceptor(String token) { + checkNotNull(token, "access_token"); + headerValue = "Bearer " + token; + } + @Override + public void apply(RequestTemplate template) { + template.header("Authorization", headerValue); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java new file mode 100644 index 0000000000..046c112a38 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java @@ -0,0 +1,67 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + + +/** + * This class represents the data that are required to register + * the oauth application. + */ +public class RegistrationProfile { + + public String callbackUrl; + public String clientName; + public String tokenScope; + public String owner; + public String grantType; + public String applicationType; + + private static final String TAG = RegistrationProfile.class.getSimpleName(); + + public String getCallbackUrl() { + return callbackUrl; + } + + public void setCallbackUrl(String callBackUrl) { + this.callbackUrl = callBackUrl; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getTokenScope() { + return tokenScope; + } + + public void setTokenScope(String tokenScope) { + this.tokenScope = tokenScope; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getGrantType() { + return grantType; + } + + public void setGrantType(String grantType) { + this.grantType = grantType; + } + + public String getApplicationType() { + return applicationType; + } + + public void setApplicationType(String applicationType) { + this.applicationType = applicationType; + } + +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java new file mode 100644 index 0000000000..570aeda7f3 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java @@ -0,0 +1,16 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +@Path("/token") +public interface TokenIssuerService { + + @POST + @Produces(MediaType.APPLICATION_JSON) + AccessTokenInfo getToken(@QueryParam("grant_type") String grant, @QueryParam("username") String username, + @QueryParam("password") String password); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/mic.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/mic.png new file mode 100644 index 0000000000000000000000000000000000000000..c7996102950c849c7cae6f05111fd6b6eddf7ea8 GIT binary patch literal 26775 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

gz%YCiQxXTmn5NU9w;Ao2 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

8wQEx_ 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 + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml new file mode 100644 index 0000000000..0650eddb59 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml new file mode 100644 index 0000000000..a02e11ade1 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + +