1+ package io.openremote.orlib.service
2+
3+ import android.Manifest
4+ import android.annotation.SuppressLint
5+ import android.app.Activity
6+ import android.bluetooth.BluetoothAdapter
7+ import android.bluetooth.BluetoothManager
8+ import android.content.Context
9+ import android.content.Intent
10+ import android.content.pm.PackageManager
11+ import android.os.Build
12+ import android.util.Log
13+ import androidx.annotation.VisibleForTesting
14+ import androidx.core.app.ActivityCompat
15+ import io.openremote.orlib.R
16+ import io.openremote.orlib.service.espprovision.CallbackChannel
17+ import io.openremote.orlib.service.espprovision.DeviceConnection
18+ import io.openremote.orlib.service.espprovision.DeviceProvision
19+ import io.openremote.orlib.service.espprovision.DeviceRegistry
20+ import io.openremote.orlib.service.espprovision.WifiProvisioner
21+ import kotlinx.coroutines.CoroutineScope
22+ import kotlinx.coroutines.Dispatchers
23+ import kotlinx.coroutines.launch
24+ import java.net.URL
25+
26+ object ESPProvisionProviderActions {
27+ const val PROVIDER_INIT = " PROVIDER_INIT"
28+ const val PROVIDER_ENABLE = " PROVIDER_ENABLE"
29+ const val PROVIDER_DISABLE = " PROVIDER_DISABLE"
30+ const val START_BLE_SCAN = " START_BLE_SCAN"
31+ const val STOP_BLE_SCAN = " STOP_BLE_SCAN"
32+ const val CONNECT_TO_DEVICE = " CONNECT_TO_DEVICE"
33+ const val DISCONNECT_FROM_DEVICE = " DISCONNECT_FROM_DEVICE"
34+ const val START_WIFI_SCAN = " START_WIFI_SCAN"
35+ const val STOP_WIFI_SCAN = " STOP_WIFI_SCAN"
36+ const val SEND_WIFI_CONFIGURATION = " SEND_WIFI_CONFIGURATION"
37+ const val PROVISION_DEVICE = " PROVISION_DEVICE"
38+ const val EXIT_PROVISIONING = " EXIT_PROVISIONING"
39+ }
40+
41+ class ESPProvisionProvider (val context : Context , val apiURL : URL = URL ("http://localhost:8080/api/master")) {
42+ @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
43+ val deviceRegistry: DeviceRegistry
44+ var deviceConnection: DeviceConnection ? = null
45+
46+ private var searchDeviceTimeout: Long = 120
47+ private var searchDeviceMaxIterations = 25
48+
49+ var wifiProvisioner: WifiProvisioner ? = null
50+ private var searchWifiTimeout: Long = 120
51+ private var searchWifiMaxIterations = 25
52+
53+ init {
54+ deviceRegistry = DeviceRegistry (context, searchDeviceTimeout, searchDeviceMaxIterations)
55+ }
56+
57+ interface ESPProvisionCallback {
58+ fun accept (responseData : Map <String , Any >)
59+ }
60+
61+ companion object {
62+ private const val espProvisionDisabledKey = " espProvisionDisabled"
63+ private const val version = " beta"
64+
65+ const val TAG = " ESPProvisionProvider"
66+
67+ const val ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE = 655
68+ const val BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE = 656
69+ }
70+
71+ private val bluetoothAdapter: BluetoothAdapter by lazy {
72+ val bluetoothManager =
73+ context.getSystemService(Context .BLUETOOTH_SERVICE ) as BluetoothManager
74+ bluetoothManager.adapter
75+ }
76+
77+ fun initialize (): Map <String , Any > {
78+ val sharedPreferences =
79+ context.getSharedPreferences(context.getString(R .string.app_name), Context .MODE_PRIVATE )
80+
81+ return hashMapOf(
82+ " action" to ESPProvisionProviderActions .PROVIDER_INIT ,
83+ " provider" to " espprovision" ,
84+ " version" to version,
85+ " requiresPermission" to true ,
86+ " hasPermission" to hasPermission(),
87+ " success" to true ,
88+ " enabled" to false ,
89+ " disabled" to sharedPreferences.contains(espProvisionDisabledKey)
90+ )
91+ }
92+
93+ @SuppressLint(" MissingPermission" )
94+ fun enable (callback : ESPProvisionCallback , activity : Activity ) {
95+ deviceRegistry.callbackChannel = CallbackChannel (callback, " espprovision" )
96+ deviceRegistry.enable()
97+
98+ if (! bluetoothAdapter.isEnabled) {
99+ Log .d(" ESP" , " BLE not enabled" )
100+ val enableBtIntent = Intent (BluetoothAdapter .ACTION_REQUEST_ENABLE )
101+ activity.startActivityForResult(enableBtIntent,
102+ ESPProvisionProvider .Companion .ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE
103+ )
104+ } else if (! hasPermission()) {
105+ Log .d(" ESP" , " Does not have permissions" )
106+ requestPermissions(activity)
107+ }
108+
109+
110+ if (bluetoothAdapter.isEnabled && hasPermission()) {
111+ providerEnabled(deviceRegistry.callbackChannel)
112+ }
113+ }
114+
115+ fun providerEnabled (callbackChannel : CallbackChannel ? ) {
116+ val sharedPreferences =
117+ context.getSharedPreferences(
118+ context.getString(R .string.app_name),
119+ Context .MODE_PRIVATE
120+ )
121+
122+ sharedPreferences.edit()
123+ .remove(espProvisionDisabledKey)
124+ .apply ()
125+
126+ callbackChannel?.sendMessage(ESPProvisionProviderActions .PROVIDER_ENABLE ,
127+ hashMapOf(
128+ " hasPermission" to hasPermission(),
129+ " success" to true ,
130+ " enabled" to true ,
131+ " disabled" to sharedPreferences.contains(espProvisionDisabledKey)
132+ )
133+ )
134+ }
135+
136+ @SuppressLint(" MissingPermission" )
137+ fun disable (): Map <String , Any > {
138+ deviceRegistry.disable()
139+
140+ // disconnectFromDevice()
141+
142+ val sharedPreferences =
143+ context.getSharedPreferences(context.getString(R .string.app_name), Context .MODE_PRIVATE )
144+ sharedPreferences.edit()
145+ .putBoolean(espProvisionDisabledKey, true )
146+ .apply ()
147+
148+ return hashMapOf(
149+ " action" to ESPProvisionProviderActions .PROVIDER_DISABLE ,
150+ " provider" to " espprovision"
151+ )
152+ }
153+
154+ @SuppressLint(" MissingPermission" )
155+ fun onRequestPermissionsResult (
156+ activity : Activity ,
157+ requestCode : Int ,
158+ prefix : String?
159+ ) {
160+ Log .d(" espprovision" , " onRequestPermissionsResult called with prefix >" + prefix + " <" )
161+ if (requestCode == BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE ) {
162+ val hasPermission = hasPermission()
163+ if (hasPermission) {
164+ if (! bluetoothAdapter.isEnabled) {
165+ val enableBtIntent = Intent (BluetoothAdapter .ACTION_REQUEST_ENABLE )
166+ activity.startActivityForResult(enableBtIntent, ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE )
167+ } else {
168+ providerEnabled(deviceRegistry.callbackChannel)
169+ if (prefix != null ) {
170+ deviceRegistry.startDevicesScan(prefix)
171+ }
172+ }
173+ }
174+ } else if (requestCode == ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE ) {
175+ if (bluetoothAdapter.isEnabled) {
176+ providerEnabled(deviceRegistry.callbackChannel)
177+ if (prefix != null ) {
178+ deviceRegistry.startDevicesScan(prefix)
179+ }
180+ }
181+ }
182+ }
183+
184+ // Device scan
185+
186+ @SuppressLint(" MissingPermission" )
187+ fun startDevicesScan (prefix : String? , activity : Activity , callback : ESPProvisionCallback ) {
188+ deviceRegistry.callbackChannel = CallbackChannel (callback, " espprovision" )
189+ if (! bluetoothAdapter.isEnabled) {
190+ Log .d(" ESP" , " BLE not enabled" )
191+ val enableBtIntent = Intent (BluetoothAdapter .ACTION_REQUEST_ENABLE )
192+ activity.startActivityForResult(enableBtIntent,
193+ ESPProvisionProvider .Companion .ENABLE_BLUETOOTH_ESPPROVISION_REQUEST_CODE
194+ )
195+ } else if (! hasPermission()) {
196+ Log .d(" ESP" , " Does not have permissions" )
197+ requestPermissions(activity)
198+ } else {
199+ deviceRegistry.startDevicesScan(prefix)
200+ }
201+ }
202+
203+ @SuppressLint(" MissingPermission" )
204+ fun stopDevicesScan () {
205+ deviceRegistry.stopDevicesScan()
206+ }
207+
208+ // MARK: Device connect/disconnect
209+
210+ @SuppressLint(" MissingPermission" )
211+ fun connectTo (deviceId : String , pop : String? = null, username : String? = null) {
212+ if (deviceConnection == null ) {
213+ deviceConnection = DeviceConnection (deviceRegistry, deviceRegistry.callbackChannel)
214+ }
215+ deviceConnection?.connectTo(deviceId, pop, username)
216+ }
217+
218+ fun disconnectFromDevice () {
219+ wifiProvisioner?.stopWifiScan()
220+ deviceConnection?.disconnectFromDevice()
221+ }
222+
223+ fun exitProvisioning () {
224+ if (deviceConnection == null ) {
225+ return
226+ }
227+ if (! deviceConnection!! .isConnected) {
228+ sendExitProvisioningError(ESPProviderErrorCode .NOT_CONNECTED , " No connection established to device" )
229+ return
230+ }
231+ deviceConnection!! .exitProvisioning()
232+ deviceRegistry?.callbackChannel?.sendMessage(
233+ ESPProvisionProviderActions .EXIT_PROVISIONING ,
234+ mapOf (" exit" to true )
235+ )
236+ }
237+
238+ private fun sendExitProvisioningError (error : ESPProviderErrorCode , errorMessage : String? ) {
239+ val data = mutableMapOf<String , Any >()
240+
241+ data[" exit" ] = false
242+ data[" errorCode" ] = error.code
243+ errorMessage?.let {
244+ data[" errorMessage" ] = it
245+ }
246+
247+ deviceRegistry?.callbackChannel?.sendMessage(ESPProvisionProviderActions .EXIT_PROVISIONING , data)
248+ }
249+
250+ // Wifi scan
251+
252+ fun startWifiScan () {
253+ if (wifiProvisioner == null ) {
254+ wifiProvisioner = WifiProvisioner (deviceConnection, deviceRegistry.callbackChannel, searchWifiTimeout, searchWifiMaxIterations)
255+ }
256+ wifiProvisioner!! .startWifiScan()
257+ }
258+
259+ fun stopWifiScan () {
260+ wifiProvisioner?.stopWifiScan()
261+ }
262+
263+ fun sendWifiConfiguration (ssid : String , password : String ) {
264+ if (wifiProvisioner == null ) {
265+ wifiProvisioner = WifiProvisioner (deviceConnection, deviceRegistry.callbackChannel, searchWifiTimeout, searchWifiMaxIterations)
266+ }
267+ wifiProvisioner!! .sendWifiConfiguration(ssid, password)
268+ }
269+
270+ // OR Configuration
271+
272+ fun provisionDevice (userToken : String ) {
273+ val deviceProvision = DeviceProvision (deviceConnection, deviceRegistry.callbackChannel, apiURL)
274+ CoroutineScope (Dispatchers .IO ).launch {
275+ deviceProvision.provision(userToken)
276+ }
277+ }
278+
279+ private fun requestPermissions (activity : Activity ) {
280+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
281+ ActivityCompat .requestPermissions(
282+ activity,
283+ arrayOf(
284+ Manifest .permission.BLUETOOTH_SCAN ,
285+ Manifest .permission.BLUETOOTH_CONNECT
286+ ),
287+ ESPProvisionProvider .Companion .BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE
288+ )
289+ } else {
290+ ActivityCompat .requestPermissions(
291+ activity,
292+ arrayOf(Manifest .permission.ACCESS_FINE_LOCATION ),
293+ ESPProvisionProvider .Companion .BLUETOOTH_PERMISSION_ESPPROVISION_REQUEST_CODE
294+ )
295+ }
296+ }
297+
298+ private fun hasPermission () = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
299+ context.checkSelfPermission(Manifest .permission.BLUETOOTH_SCAN ) == PackageManager .PERMISSION_GRANTED &&
300+ context.checkSelfPermission(Manifest .permission.BLUETOOTH_CONNECT ) == PackageManager .PERMISSION_GRANTED
301+ } else {
302+ context.checkSelfPermission(Manifest .permission.ACCESS_FINE_LOCATION ) == PackageManager .PERMISSION_GRANTED
303+ }
304+
305+ }
306+
307+ data class ESPProviderException (val errorCode : ESPProviderErrorCode , val errorMessage : String ) : Exception()
308+
309+ enum class ESPProviderErrorCode (val code : Int ) {
310+ UNKNOWN_DEVICE (100 ),
311+
312+ BLE_COMMUNICATION_ERROR (200 ),
313+
314+ NOT_CONNECTED (300 ),
315+ COMMUNICATION_ERROR (301 ),
316+
317+ SECURITY_ERROR (400 ),
318+
319+ WIFI_CONFIGURATION_ERROR (500 ),
320+ WIFI_COMMUNICATION_ERROR (501 ),
321+ WIFI_AUTHENTICATION_ERROR (502 ),
322+ WIFI_NETWORK_NOT_FOUND (503 ),
323+
324+ TIMEOUT_ERROR (600 ),
325+
326+ GENERIC_ERROR (10000 );
327+ }
0 commit comments