Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Doug's work on Thread. #172

Merged
merged 1 commit into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}

pierredelisle marked this conversation as resolved.
Show resolved Hide resolved
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
Loading