Skip to content

Commit

Permalink
Integrate Doug's work on Thread. (#172)
Browse files Browse the repository at this point in the history
Note that ktfmt formatted many files that were not targetted by this PR.
  • Loading branch information
pierredelisle authored Sep 22, 2023
1 parent 4303805 commit c9fa75d
Show file tree
Hide file tree
Showing 17 changed files with 1,179 additions and 48 deletions.
10 changes: 10 additions & 0 deletions 3p-ecosystem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ dependencies {
implementation(libs.play.services.base)
implementation(libs.play.services.home)

// Thread Network
implementation(libs.play.services.threadnetwork)
// Thread QR Code Scanning
implementation(libs.code.scanner)
// Thread QR Code Generation
implementation(libs.zxing)


// AndroidX
implementation(libs.appcompat)
implementation(libs.constraintlayout)
Expand Down Expand Up @@ -207,6 +215,8 @@ dependencies {
// Other
implementation(libs.material)
implementation(libs.timber)
// Needed for using BaseEncoding class
implementation(libs.guava)

// Test
testImplementation(libs.junit)
Expand Down
9 changes: 8 additions & 1 deletion 3p-ecosystem/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GHSAFM">
android:theme="@style/Theme.GHSAFM"
android:usesCleartextTraffic="true">
<!-- usesCleartextTraffic needed for OTBR local unencrypted communication -->
<activity
android:name="com.google.homesampleapp.MainActivity"
android:exported="true">
Expand Down Expand Up @@ -79,6 +81,11 @@
<meta-data android:name="home:0:preferred" android:value=""/>
</service>

<!-- GPS automatically downloads scanner module when app is installed -->
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>

</application>

</manifest>
29 changes: 29 additions & 0 deletions 3p-ecosystem/src/main/java/com/google/homesampleapp/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package com.google.homesampleapp
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Looper
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import com.google.protobuf.Timestamp
import java.io.File
import java.lang.Long.max
Expand Down Expand Up @@ -354,3 +357,29 @@ enum class CommissioningWindowStatus(val status: Int) {
}

val OPEN_COMMISSIONING_WINDOW_API = OpenCommissioningWindowApi.ChipDeviceController

/**
* ToastTimber logs the same message on both Timber and Toast, thus giving some feedback to the user
* that doesn't have ADB connected
*/
object ToastTimber {
fun d(msg: String, activity: FragmentActivity) {
Timber.d(msg)
checkLooper()
Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show()
}

fun e(msg: String, activity: FragmentActivity) {
Timber.e(msg)
checkLooper()
Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show()
}

/**
* Asserts Looper is running in the current thread. Important when using Timber in coroutine
* Threads that don't have a Looper running
*/
private fun checkLooper() {
if (Looper.myLooper() == null) Looper.prepare()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class ChipClient @Inject constructor(@ApplicationContext context: Context) {
java.lang.IllegalStateException(
"Failed unpairing device [$nodeId] with status [$status]"))
}

override fun onSuccess(nodeId: Long) {
Timber.d("awaitUnpairDevice.onSuccess: deviceId [$nodeId]")
continuation.resume(Unit)
Expand Down Expand Up @@ -131,6 +132,7 @@ class ChipClient @Inject constructor(@ApplicationContext context: Context) {
super.onConnectDeviceComplete()
continuation.resume(Unit)
}

// Note that an error in processing is not necessarily communicated via onError().
// onCommissioningComplete with a "code != 0" also denotes an error in processing.
override fun onPairingComplete(code: Int) {
Expand Down Expand Up @@ -186,6 +188,7 @@ class ChipClient @Inject constructor(@ApplicationContext context: Context) {
continuation.resume(Unit)
}
}

override fun onError(error: Throwable) {
super.onError(error)
continuation.resumeWithException(error)
Expand Down Expand Up @@ -213,6 +216,7 @@ class ChipClient @Inject constructor(@ApplicationContext context: Context) {
java.lang.IllegalStateException(
"Failed opening the pairing window with status [${status}]"))
}

override fun onSuccess(deviceId: Long, manualPairingCode: String?, qrCode: String?) {
Timber.d(
"ShareDevice: awaitOpenPairingWindowWithPIN.onSuccess: deviceId [${deviceId}]")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess(values: MutableList<Int>?) {
continuation.resume(values)
}

override fun onError(ex: Exception) {
continuation.resumeWithException(ex)
}
Expand Down Expand Up @@ -159,6 +160,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
) {
continuation.resume(values)
}

override fun onError(ex: Exception) {
continuation.resumeWithException(ex)
}
Expand Down Expand Up @@ -204,6 +206,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess(values: MutableList<Long>) {
continuation.resume(values)
}

override fun onError(ex: Exception) {
continuation.resumeWithException(ex)
}
Expand All @@ -220,6 +223,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess(values: MutableList<Long>) {
continuation.resume(values)
}

override fun onError(ex: Exception) {
continuation.resumeWithException(ex)
}
Expand Down Expand Up @@ -252,6 +256,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess(value: MutableList<Long>) {
continuation.resume(value)
}

override fun onError(ex: Exception) {
continuation.resumeWithException(ex)
}
Expand Down Expand Up @@ -284,6 +289,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess(value: Int?) {
continuation.resume(value)
}

override fun onError(ex: Exception) {
continuation.resumeWithException(ex)
}
Expand All @@ -307,6 +313,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess(values: MutableList<Long>) {
continuation.resume(values)
}

override fun onError(ex: Exception) {
continuation.resumeWithException(ex)
}
Expand Down Expand Up @@ -469,13 +476,15 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess() {
continuation.resume(Unit)
}

override fun onError(ex: Exception) {
Timber.e(ex, "readOnOffAttribute command failure")
continuation.resumeWithException(ex)
}
})
}
}

// CODELAB FEATURED END

suspend fun setOnOffDeviceStateOnOffCluster(deviceId: Long, isOn: Boolean, endpoint: Int) {
Expand All @@ -498,6 +507,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
Timber.d("Success for setOnOffDeviceStateOnOffCluster")
continuation.resume(Unit)
}

override fun onError(ex: Exception) {
Timber.e(ex, "Failure for setOnOffDeviceStateOnOffCluster")
continuation.resumeWithException(ex)
Expand All @@ -514,6 +524,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
Timber.d("Success for getOnOffDeviceStateOnOffCluster")
continuation.resume(Unit)
}

override fun onError(ex: Exception) {
Timber.e(ex, "Failure for getOnOffDeviceStateOnOffCluster")
continuation.resumeWithException(ex)
Expand All @@ -540,6 +551,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
Timber.d("readOnOffAttribute success: [$value]")
continuation.resume(value)
}

override fun onError(ex: Exception) {
Timber.e(ex, "readOnOffAttribute command failure")
continuation.resumeWithException(ex)
Expand Down Expand Up @@ -584,6 +596,7 @@ class ClustersHelper @Inject constructor(private val chipClient: ChipClient) {
override fun onSuccess() {
continuation.resume(Unit)
}

override fun onError(ex: java.lang.Exception?) {
Timber.e(
ex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ class SettingsDeveloperUtilitiesNestedFragment : PreferenceFragmentCompat() {
}
}

// Thread network.
val threadPreference: Preference? = findPreference("thread")
threadPreference?.setOnPreferenceClickListener {
Timber.d("threadPreference onPreferenceClickListener()")
findNavController().navigate(R.id.action_settingsDeveloperUtilitiesFragment_to_threadFragment)
true
}

// Log Repositories.
val logReposPreference: Preference? = findPreference("logrepos")
logReposPreference?.setOnPreferenceClickListener {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.google.homesampleapp.screens.thread

import androidx.fragment.app.FragmentActivity
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
import com.google.common.io.BaseEncoding
import java.io.BufferedInputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import timber.log.Timber

/**
* OtbrHttp object has some useful methods, enums and properties for getting/setting information to
* OpenThread Border Routers using the HTTP REST API:
* https://github.com/openthread/ot-br-posix/blob/main/src/rest/openapi.yaml
*/
object OtbrHttpClient {
enum class Verbs {
GET,
POST,
PUT,
DELETE
}

/** what is generally an OK response */
val okResponses =
setOf(
HttpURLConnection.HTTP_OK,
HttpURLConnection.HTTP_CREATED,
HttpURLConnection.HTTP_ACCEPTED,
HttpURLConnection.HTTP_NOT_AUTHORITATIVE,
HttpURLConnection.HTTP_NO_CONTENT,
HttpURLConnection.HTTP_RESET,
HttpURLConnection.HTTP_PARTIAL)

/**
* Creates credentials in the format used by the OTBR HTTP server. See its documentation in
* https://github.com/openthread/ot-br-posix/blob/main/src/rest/openapi.yaml#L215
*/
fun createJsonCredentialsObject(newCredentials: ThreadNetworkCredentials): JSONObject {
val jsonTimestamp = JSONObject()
jsonTimestamp.put("Seconds", System.currentTimeMillis() / 1000)
jsonTimestamp.put("Ticks", 0)
jsonTimestamp.put("Authoritative", false)

val jsonQuery = JSONObject()
jsonQuery.put(
"ActiveDataset", BaseEncoding.base16().encode(newCredentials.activeOperationalDataset))
jsonQuery.put("PendingTimestamp", jsonTimestamp)
// delay of committing the pending set into active set: 10000ms
jsonQuery.put("Delay", 10000)

Timber.d(jsonQuery.toString())

return jsonQuery
}

/**
* Suspend function that generically creates an HTTP request and gives back its response. This was
* created mostly for interacting with the OTBR HTTP, but it is generic enough for general use.
*
* When using a GET for a credential, you may either use [acceptMimeType] == "text/plain", which
* will return the credentials in hex/base16 TLV format, or [acceptMimeType] ==
* "application/json", which will return the credentials in JSON format. See the OpenThread HTTP
* Rest API for more information (link above)
*/
suspend fun createJsonHttpRequest(
url: URL,
activity: FragmentActivity,
verb: Verbs,
postPayload: String = "",
contentTypeMimeType: String = "application/json",
acceptMimeType: String = "application/json",
): Pair<HttpURLConnection, String> {

// Use IO Dispatcher so it doesn't block the UI thread
return withContext(Dispatchers.IO) {
// typecasting the connection exposes additional methods and properties
val urlConnection = url.openConnection() as HttpURLConnection
var inputStream: InputStream? = null
var content = ""

urlConnection.connectTimeout = 1000 /* ms */
urlConnection.setRequestProperty("Accept", acceptMimeType)
urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0")

urlConnection.requestMethod =
when (verb) {
Verbs.GET -> "GET"
Verbs.POST -> "POST"
Verbs.DELETE -> "DELETE"
Verbs.PUT -> "PUT"
}

when (verb) {
Verbs.POST,
Verbs.PUT -> {
urlConnection.setRequestProperty("Content-Type", contentTypeMimeType)
urlConnection.doOutput = true
try {
val outputStream = urlConnection.outputStream
postPayload?.let { outputStream.write(postPayload.toString().toByteArray()) }
outputStream.flush()
outputStream.close()

val response = urlConnection.responseCode
Timber.d("$verb response: $response")
inputStream = BufferedInputStream(urlConnection.inputStream)
content = String(inputStream.readBytes(), StandardCharsets.UTF_8)
} catch (e: Exception) {
Timber.e("Error loading page $e")
} finally {
inputStream?.let { inputStream!!.close() }
}
}
Verbs.GET,
Verbs.DELETE -> {
try {
inputStream = BufferedInputStream(urlConnection.inputStream)
content = String(inputStream.readBytes(), StandardCharsets.UTF_8)
val response = urlConnection.responseCode
Timber.d("$verb response: $response")
} catch (e: Exception) {
Timber.e("Error loading page $e")
} finally {
inputStream?.let { inputStream.close() }
}
}
else -> {
Timber.e("HTTP verb not supported")
}
}

Pair(urlConnection, content)
}
}
}
Loading

0 comments on commit c9fa75d

Please sign in to comment.